chore: import upstream snapshot with attribution
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
name: Auto-close duplicate issues
|
||||
description: Auto-closes issues that are duplicates of existing issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 * * *" # Run daily at 9 AM UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
auto-close-duplicates:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Auto-close duplicate issues
|
||||
run: uv run scripts/auto_close_duplicates.py
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.marvin-token.outputs.token }}
|
||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Auto-close needs MRE issues
|
||||
description: Auto-closes issues that need minimal reproducible examples after 7 days of author inactivity
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 * * *" # Run daily at 9 AM UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
auto-close-needs-mre:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Auto-close needs MRE issues
|
||||
run: uv run scripts/auto_close_needs_mre.py
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.marvin-token.outputs.token }}
|
||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
|
||||
@@ -0,0 +1,197 @@
|
||||
name: Marvin Test Failure Analysis
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Tests", "Run static analysis"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: marvin-test-failure-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
martian-test-failure:
|
||||
# Only run if the test workflow failed
|
||||
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
# Install UV package manager
|
||||
- name: Install UV
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
# Install dependencies
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-packages --group dev
|
||||
|
||||
- name: Set analysis prompt
|
||||
id: analysis-prompt
|
||||
run: |
|
||||
cat >> $GITHUB_OUTPUT << 'EOF'
|
||||
PROMPT<<PROMPT_END
|
||||
You're a test failure analysis assistant for FastMCP, a Python framework for building Model Context Protocol servers and clients.
|
||||
|
||||
# Your Task
|
||||
A GitHub Actions workflow has failed. Your job is to:
|
||||
1. Analyze the test failure(s) to understand what went wrong
|
||||
2. Identify the root cause of the failure(s)
|
||||
3. Suggest a clear, actionable solution to fix the failure(s)
|
||||
|
||||
# Response Proportionality
|
||||
Match your response length to the complexity of the failure. Not every failure needs a full investigation:
|
||||
|
||||
**Trivial failures** (formatting, linting) — post a short, direct comment. No collapsible sections, no root-cause deep-dive. Example:
|
||||
> CI failed: `ruff format` reformatted 2 files. Run `uv run ruff format .` locally and push.
|
||||
|
||||
**Pre-existing flaky tests** unrelated to the PR — say so briefly. Don't write a full analysis of a test the PR didn't touch. Example:
|
||||
> CI failed due to a pre-existing flaky test (`test_name`) unrelated to this PR's changes. Safe to re-run.
|
||||
|
||||
**Real failures caused by the PR** — these deserve the full analysis format below. Spend your effort here.
|
||||
|
||||
# Getting Started
|
||||
1. Call the generate_agents_md tool to get a high-level summary of the project
|
||||
2. Get the pull request associated with this workflow run from the GitHub repository: ${{ github.repository }}
|
||||
- The workflow run ID is: ${{ github.event.workflow_run.id }}
|
||||
- The workflow run was triggered by: ${{ github.event.workflow_run.event }}
|
||||
- Use GitHub MCP tools to get PR details and workflow run information
|
||||
3. Use the GitHub MCP tools to fetch job logs and failure information:
|
||||
- Use get_workflow_run to get details about the failed workflow
|
||||
- Use list_workflow_jobs to see which jobs failed
|
||||
- Use get_job_logs with failed_only=true to get logs for failed jobs
|
||||
- Use summarize_run_log_failures to get an AI summary of what failed
|
||||
4. Analyze the failures to understand the root cause
|
||||
5. Search the codebase for relevant files, tests, and implementations
|
||||
|
||||
# Your Response
|
||||
Post a comment on the pull request with your analysis.
|
||||
|
||||
Lead with a tl;dr — 1-2 sentences that tell the developer what broke and what to do about it. This should be visible without expanding anything.
|
||||
|
||||
Push supporting detail into collapsible `<details>` blocks. The reader should be able to act on your comment without expanding a single one. Think of details blocks as appendices — there if someone wants to dig deeper, not required for the main message.
|
||||
|
||||
For real (non-trivial) failures, use this structure:
|
||||
|
||||
**tl;dr**: What failed and what to do (1-2 sentences, always visible)
|
||||
|
||||
**Root Cause**: Why it failed (a short paragraph, always visible)
|
||||
|
||||
**Fix**: Specific files and changes needed (always visible)
|
||||
|
||||
<details>
|
||||
<summary>Log excerpts</summary>
|
||||
Relevant failure output
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Related files</summary>
|
||||
Files relevant to the failure
|
||||
</details>
|
||||
|
||||
# Quality Standards
|
||||
- Every claim needs evidence: file paths, line numbers, log excerpts. Never say "the test fails" without citing which test and what the error was.
|
||||
- Focus on facts from the logs and code, not speculation. If you can't determine the root cause, say so clearly — "I don't know" is better than a wrong diagnosis.
|
||||
- If your only suggestion is a bad one (disable the test, increase the timeout, etc.), say so honestly rather than dressing it up.
|
||||
- Do not paste raw CLI output (e.g., prek progress bars, pytest collection output) into the comment body. Quote only the relevant failure lines.
|
||||
- Always include specific file names, tool names, and test names in your summary. Never leave a sentence with a blank where a name should be.
|
||||
|
||||
# Self-Review Before Posting
|
||||
Before posting your comment, re-read it as the PR author would. Ask:
|
||||
- Can I act on this without expanding any `<details>` block?
|
||||
- Does every claim cite a specific file, line, or log excerpt?
|
||||
- Am I telling them something they can't already see in the CI logs, or just restating them?
|
||||
If your comment doesn't add value beyond what the logs already show, don't post it.
|
||||
|
||||
# STOP SIGNALS
|
||||
If anyone on the PR has asked the bot to stop — e.g., "stop", "go away", "don't comment", "no more bot comments" — exit immediately without further action. This includes past comments in the thread, not just the most recent one.
|
||||
|
||||
If you are posting the same suggestion as you have previously made, do not post the suggestion again.
|
||||
|
||||
# IMPORTANT: EDIT YOUR COMMENT
|
||||
Do not post a new comment every time you triage a failing workflow. If a previous comment has been posted by you (marvin)
|
||||
in a previous triage, edit that comment do not add a new comment for each failure. Be sure to include a note that you've edited
|
||||
your comment to reflect the latest analysis. Don't worry about keeping the old content around, there's comment history for
|
||||
that.
|
||||
|
||||
# Available Tools
|
||||
- You can run make commands (e.g., `make lint`, `make typecheck`, `make sync`) to build, test, or lint the code
|
||||
- You can also run git commands (e.g., `git status`, `git log`, `git diff`) to inspect the repository
|
||||
- You can use WebSearch and WebFetch to research errors, stack traces, or related issues
|
||||
- For bash commands, you are limited to make and git commands only
|
||||
|
||||
# Problems Encountered
|
||||
If you encounter any problems during your analysis (e.g., unable to fetch logs, tools not working), document them clearly so the team knows what limitations you faced.
|
||||
PROMPT_END
|
||||
EOF
|
||||
|
||||
- name: Setup GitHub MCP Server
|
||||
run: |
|
||||
mkdir -p /tmp/mcp-config
|
||||
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"repository-summary": {
|
||||
"type": "http",
|
||||
"url": "https://agents-md-generator.fastmcp.app/mcp"
|
||||
},
|
||||
"code-search": {
|
||||
"type": "http",
|
||||
"url": "https://public-code-search.fastmcp.app/mcp"
|
||||
},
|
||||
"github-research": {
|
||||
"type": "stdio",
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"github-research-mcp"
|
||||
],
|
||||
"env": {
|
||||
"DISABLE_SUMMARIES": "true",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Clean up stale Claude locks
|
||||
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
github_token: ${{ steps.marvin-token.outputs.token }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
|
||||
bot_name: "Marvin Context Protocol"
|
||||
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
prompt: ${{ steps.analysis-prompt.outputs.PROMPT }}
|
||||
claude_args: |
|
||||
--allowed-tools mcp__repository-summary,mcp__code-search,mcp__github-research,WebSearch,WebFetch,Bash(make:*,git:*)
|
||||
--mcp-config /tmp/mcp-config/mcp-servers.json
|
||||
@@ -0,0 +1,221 @@
|
||||
# Triage new issues: investigate, recommend, apply labels
|
||||
# Calls run-claude directly with triage prompt (elastic issue-triage style)
|
||||
|
||||
name: Triage Issue
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
if: |
|
||||
github.event.issue.user.login == 'strawgate' ||
|
||||
(github.event.issue.user.login == 'jlowin' && contains(toJSON(github.event.issue.labels.*.name), 'bug'))
|
||||
concurrency:
|
||||
group: triage-issue-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: React to issue with eyes
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
|
||||
run: |
|
||||
gh api "repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/reactions" -f content=eyes 2>/dev/null || true
|
||||
|
||||
- name: Run Claude for Triage
|
||||
uses: ./.github/actions/run-claude
|
||||
env:
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
with:
|
||||
claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github-token: ${{ steps.marvin-token.outputs.token }}
|
||||
allowed-tools: "Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code"
|
||||
prompt: |
|
||||
<context>
|
||||
Repository: ${{ github.repository }}
|
||||
Issue Number: #${{ github.event.issue.number }}
|
||||
Issue Title: ${{ env.ISSUE_TITLE }}
|
||||
Issue Author: ${{ github.event.issue.user.login }}
|
||||
</context>
|
||||
|
||||
<issue_body>
|
||||
${{ env.ISSUE_BODY }}
|
||||
</issue_body>
|
||||
|
||||
<task>
|
||||
Triage this new GitHub issue and provide a helpful, actionable response. You can write files and execute commands to test, verify, or investigate the issue.
|
||||
</task>
|
||||
|
||||
<constraints>
|
||||
This workflow is for investigation, testing, and planning.
|
||||
|
||||
You CANNOT: Create branches, checkout branches, commit code to the repository
|
||||
Do not push changes to the repository.
|
||||
You CAN: Read/analyze code, search repository, review git history, search for similar issues, write files, verify behavior, provide analysis and recommendations
|
||||
</constraints>
|
||||
|
||||
<allowed_tools>
|
||||
You have access to the following tools (comma-separated list):
|
||||
|
||||
Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code
|
||||
|
||||
You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.
|
||||
</allowed_tools>
|
||||
|
||||
<getting_started>
|
||||
Use `mcp__agents-md-generator__generate_agents_md` to get repository context before triaging.
|
||||
</getting_started>
|
||||
|
||||
<investigation_tools>
|
||||
- `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)
|
||||
- `WebSearch`: Search the web for documentation, best practices, or solutions
|
||||
- `WebFetch`: Fetch and read content from URLs
|
||||
- Git commands: You have access to git commands, but write commands (commit, push, checkout, branch creation) are blocked
|
||||
- Write: You can write files (e.g., test files, temporary files for verification)
|
||||
- Execution: See `<allowed_tools>` section above for exact list of available execution commands
|
||||
</investigation_tools>
|
||||
|
||||
<execution_guidelines>
|
||||
If execution commands are available (check `<allowed_tools>` section), you can:
|
||||
- Run tests to verify reported bugs or test proposed solutions
|
||||
- Execute scripts to understand behavior
|
||||
- Run linters or static analysis tools
|
||||
- Verify environment setup or dependencies
|
||||
- Test specific code paths or scenarios
|
||||
- Write test files to confirm behavior
|
||||
|
||||
When executing commands:
|
||||
- Explain what you're testing and why
|
||||
- Include command output in your response when relevant
|
||||
- Use execution to validate your findings and recommendations
|
||||
- Only use commands that are explicitly listed in `<allowed_tools>`
|
||||
</execution_guidelines>
|
||||
|
||||
<response_goals>
|
||||
Your number one priority is to provide a great response to the issue. A great response is a response that is clear, concise, accurate, and actionable. You will avoid long paragraphs, flowery language, and overly verbose responses. Your readers have limited time and attention, so you will be concise and to the point.
|
||||
|
||||
In priority order your goal is to:
|
||||
1. Provide context about the request or issue (related issues, pull requests, files, etc.)
|
||||
2. Layout a single high-quality and actionable recommendation for how to address the issue based on your knowledge of the project, codebase, and issue
|
||||
3. Provide a high quality and detailed plan that a junior developer could follow to implement the recommendation
|
||||
4. Use execution to verify findings when appropriate (check `<allowed_tools>` section for available commands)
|
||||
|
||||
Report findings and recommendations — not your process. Do not include task checklists, progress tracking, or "steps I took" narration (e.g., `- [x] Read source code`). The reader cares about what you found, not how you found it.
|
||||
</response_goals>
|
||||
|
||||
<evidence_standards>
|
||||
Every claim in your response must be grounded in evidence you can cite:
|
||||
- **Code references**: Always include file path and line number (e.g., `fastmcp_slim/fastmcp/client/client.py:142`). Never say "the client code does X" without pointing to where.
|
||||
- **Bug confirmation**: If you say a bug is real, show the specific code path that produces it. If you ran a test, include the command and output.
|
||||
- **Related items**: When citing a related issue or PR, explain specifically why it's related — not just that it exists.
|
||||
- **Confidence**: If you're uncertain about a finding, say so. "I don't know" or "I couldn't confirm this" is better than a speculative diagnosis. Only report findings you would confidently defend.
|
||||
</evidence_standards>
|
||||
|
||||
<quality_gate>
|
||||
Before posting, re-read your response as a maintainer would:
|
||||
- Does the tl;dr give the full picture without expanding anything?
|
||||
- Does every claim cite a specific file, line, or test result?
|
||||
- Is this telling the maintainer something they couldn't find in 5 minutes of reading the issue and grepping the code?
|
||||
If your response doesn't add meaningful value beyond restating the issue, it's okay to post a short "confirmed, straightforward fix in [file]:[line]" response instead of a full analysis.
|
||||
</quality_gate>
|
||||
|
||||
<response_sections>
|
||||
Populate the following sections in your response:
|
||||
Recommendation (or "No recommendation" with reason)
|
||||
Findings
|
||||
Verification (if you executed tests or commands - check `<allowed_tools>` section)
|
||||
Detailed Action Plan
|
||||
Related Items
|
||||
Related Files
|
||||
Related Webpages
|
||||
|
||||
You may not be able to do all of these things, sometimes you may find that all you can do is provide in-depth context of the issue and related items. That's perfectly acceptable and expected. Your performance is judged by how accurate your findings are, do the investigation required to have high confidence in your findings and recommendations. "I don't know" or "I'm unable to recommend a course of action" is better than a bad or wrong answer.
|
||||
|
||||
Structure: Lead with a tl;dr (1-3 sentences, always visible) that gives the reader the bottom line — what this issue is, whether it's valid, and what to do about it. The reader should be able to act on your comment without expanding anything.
|
||||
|
||||
Push everything else into collapsible `<details>` blocks: findings, verification output, action plans, related items, related files. These are appendices — valuable for someone who wants to dig deeper, but not required for the main message. The only things that should be visible without clicking are the tl;dr and the recommendation. Short responses (a few sentences) don't need collapsible sections at all.
|
||||
|
||||
</response_sections>
|
||||
<response_examples>
|
||||
# Example: the tl;dr and recommendation are always visible, everything else is collapsed
|
||||
|
||||
**tl;dr**: Confirmed bug — `Calculator.divide` raises `ValueError` instead of `DivisionByZeroError`. PR #654 partially addresses this but is incomplete.
|
||||
|
||||
**Recommendation**: Complete PR #654: update `Calculator.divide` to raise `DivisionByZeroError` and update the test assertions to match.
|
||||
|
||||
<details>
|
||||
<summary>Findings</summary>
|
||||
...details from the code analysis that are relevant to the issue and the recommendation...
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Verification</summary>
|
||||
|
||||
```bash
|
||||
$ pytest test_calculator.py::test_divide_by_zero
|
||||
FAILED - raises ValueError instead of DivisionByZeroError
|
||||
```
|
||||
This confirms the issue report is accurate.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Action Plan</summary>
|
||||
...a detailed plan that a junior developer could follow to implement the recommendation...
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Related Issues and Pull Requests</summary>
|
||||
|
||||
| Issue or PR | Relevance |
|
||||
| --- | --- |
|
||||
| [Add matrix operations support](https://github.com/PrefectHQ/fastmcp/pull/680) | Directly addresses the feature request |
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Related Files</summary>
|
||||
|
||||
| File | Relevance |
|
||||
| --- | --- |
|
||||
| [calculator.py L29-32](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py#L29-L32) | The `divide` method that raises ValueError |
|
||||
| [test_calculator.py L25-27](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py#L25-L27) | Test asserting ValueError (needs updating) |
|
||||
</details>
|
||||
</response_examples>
|
||||
|
||||
<response_footer>
|
||||
Always end your comment with a new line, three dashes, and the footer message:
|
||||
<exact_content>
|
||||
|
||||
---
|
||||
Marvin Context Protocol | Type `/marvin` to interact further
|
||||
|
||||
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
|
||||
</exact_content>
|
||||
</response_footer>
|
||||
|
||||
<github_formatting>
|
||||
When writing GitHub comments, wrap branch names, tags, or other @-references in backticks (e.g., `@main`, `@v1.0`) to avoid accidentally pinging users. Do not add backticks around terms that are already inside backticks or code blocks.
|
||||
Do not write `fixes #N`, `closes #N`, or `resolves #N` in comments — these can accidentally close issues. Use plain `#N` references instead.
|
||||
</github_formatting>
|
||||
@@ -0,0 +1,148 @@
|
||||
# Respond to /marvin mentions in issue comments (elastic mention-in-issue style)
|
||||
# Calls run-claude directly
|
||||
|
||||
name: Comment on Issue
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: |
|
||||
!github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/marvin') &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install UV
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --python 3.12
|
||||
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: React to comment with eyes
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
|
||||
run: |
|
||||
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" -f content=eyes 2>/dev/null || true
|
||||
|
||||
- name: Run Claude for Issue Comment
|
||||
uses: ./.github/actions/run-claude
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
with:
|
||||
claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github-token: ${{ steps.marvin-token.outputs.token }}
|
||||
trigger-phrase: "/marvin"
|
||||
allowed-bots: "*"
|
||||
allowed-tools: "Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code"
|
||||
prompt: |
|
||||
<context>
|
||||
Repository: ${{ github.repository }}
|
||||
Issue Number: #${{ github.event.issue.number }}
|
||||
Issue Title: ${{ env.ISSUE_TITLE }}
|
||||
Issue Author: ${{ github.event.issue.user.login }}
|
||||
Comment Author: ${{ github.event.comment.user.login }}
|
||||
</context>
|
||||
|
||||
<user_request>
|
||||
${{ env.COMMENT_BODY }}
|
||||
</user_request>
|
||||
|
||||
<task>
|
||||
You have been mentioned in a GitHub issue comment. Understand the request, gather context, complete the task, and respond with results.
|
||||
</task>
|
||||
|
||||
<constraints>
|
||||
You CAN: Read/analyze code, modify files, write code, run tests, execute commands, commit code, push changes, create branches, create pull requests
|
||||
</constraints>
|
||||
|
||||
<allowed_tools>
|
||||
You have access to the following tools (comma-separated list):
|
||||
|
||||
Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code
|
||||
|
||||
You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.
|
||||
</allowed_tools>
|
||||
|
||||
<getting_started>
|
||||
Use `mcp__agents-md-generator__generate_agents_md` to get repository context before responding.
|
||||
</getting_started>
|
||||
|
||||
<investigation_approach>
|
||||
Be thorough in your investigations:
|
||||
- Understand the full context of the repository
|
||||
- Review related code, issues, and PRs
|
||||
- Consider edge cases and implications
|
||||
- Gather all relevant information before responding
|
||||
|
||||
Available tools:
|
||||
- `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)
|
||||
- `WebSearch`: Search the web for documentation, best practices, or solutions
|
||||
- `WebFetch`: Fetch and read content from URLs
|
||||
</investigation_approach>
|
||||
|
||||
<common_tasks>
|
||||
- Answer questions about the codebase
|
||||
- Help debug reported problems
|
||||
- Suggest solutions or workarounds
|
||||
- Provide code examples
|
||||
- Help clarify requirements
|
||||
- Link to relevant documentation or code
|
||||
- Create branches, commit changes, and open PRs when asked
|
||||
</common_tasks>
|
||||
|
||||
<response_guidelines>
|
||||
- Lead with a tl;dr — the bottom line in 1-3 sentences, always visible. The reader should be able to act without expanding anything.
|
||||
- Push supporting detail (code analysis, verification output, related items) into collapsible `<details>` blocks. These are appendices, not the main message.
|
||||
- Short responses (a few sentences) don't need collapsible sections at all.
|
||||
- Be concise and actionable.
|
||||
- If the request is unclear, ask clarifying questions.
|
||||
- Report findings and recommendations — not your process. Do not include task checklists or "steps I took" narration.
|
||||
- Every claim needs evidence: cite file paths, line numbers, or command output. Never say "the code does X" without pointing to where.
|
||||
- If you're uncertain, say so. "I couldn't confirm this" is better than a speculative answer.
|
||||
</response_guidelines>
|
||||
|
||||
<github_safety>
|
||||
- Do not write `fixes #N`, `closes #N`, or `resolves #N` in comments — these can accidentally close issues.
|
||||
- When referencing issues, use plain `#N` or link syntax without action keywords.
|
||||
</github_safety>
|
||||
|
||||
<response_footer>
|
||||
Always end your comment with a new line, three dashes, and the footer message:
|
||||
<exact_content>
|
||||
|
||||
---
|
||||
Marvin Context Protocol | Type `/marvin` to interact further
|
||||
|
||||
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
|
||||
</exact_content>
|
||||
</response_footer>
|
||||
|
||||
<github_formatting>
|
||||
When writing GitHub comments, wrap branch names, tags, or other @-references in backticks (e.g., `@main`, `@v1.0`) to avoid accidentally pinging users. Do not add backticks around terms that are already inside backticks or code blocks.
|
||||
</github_formatting>
|
||||
@@ -0,0 +1,307 @@
|
||||
# Respond to /marvin mentions in PR review comments and issue comments on PRs
|
||||
# Calls run-claude directly
|
||||
|
||||
name: Comment on PR
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/marvin') &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout PR head branch
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
# do not set to pull_request.head.ref, claude will pull the branch if needed
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install UV
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --python 3.12
|
||||
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: React to comment with eyes
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
|
||||
run: |
|
||||
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" -f content=eyes 2>/dev/null || true
|
||||
|
||||
- name: Get PR HEAD SHA
|
||||
id: pr-info
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
|
||||
run: |
|
||||
PR_NUMBER="${{ github.event.issue.number }}"
|
||||
HEAD_SHA=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" --jq '.head.sha')
|
||||
echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Run Claude for PR Comment
|
||||
uses: ./.github/actions/run-claude
|
||||
env:
|
||||
MENTION_REPO: ${{ github.repository }}
|
||||
MENTION_PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
|
||||
MENTION_SCRIPTS: ${{ github.workspace }}/.github/scripts/mention
|
||||
PR_REVIEW_REPO: ${{ github.repository }}
|
||||
PR_REVIEW_PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
|
||||
PR_REVIEW_HEAD_SHA: ${{ steps.pr-info.outputs.head_sha }}
|
||||
PR_REVIEW_COMMENTS_DIR: /tmp/pr-review-comments
|
||||
PR_REVIEW_HELPERS_DIR: ${{ github.workspace }}/.github/scripts/pr-review
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
PR_TITLE: ${{ github.event.issue.title }}
|
||||
with:
|
||||
claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github-token: ${{ steps.marvin-token.outputs.token }}
|
||||
trigger-phrase: "/marvin"
|
||||
allowed-bots: "*"
|
||||
allowed-tools: "Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code"
|
||||
prompt: |
|
||||
<context>
|
||||
Repository: ${{ github.repository }}
|
||||
PR Number: #${{ steps.pr-info.outputs.pr_number }}
|
||||
PR Title: ${{ env.PR_TITLE }}
|
||||
PR Author: ${{ github.event.issue.user.login }}
|
||||
Comment Author: ${{ github.event.comment.user.login }}
|
||||
|
||||
**Note**: The PR head branch has already been checked out. The workspace is ready - you can immediately start working on the PR code.
|
||||
</context>
|
||||
|
||||
<user_request>
|
||||
${{ env.COMMENT_BODY }}
|
||||
</user_request>
|
||||
|
||||
<task>
|
||||
You have been mentioned in a Pull Request comment. Understand the request, gather context, complete the task, and respond with results.
|
||||
</task>
|
||||
|
||||
<constraints>
|
||||
You CAN: Read/analyze code, modify files, write code, run tests, execute commands, resolve review threads, commit and push changes to the PR branch, checkout branches
|
||||
You CANNOT: Create new branches unrelated to this PR, create new pull requests
|
||||
|
||||
When making changes, commit and push to the PR's head branch so the author gets the fix directly.
|
||||
</constraints>
|
||||
|
||||
<allowed_tools>
|
||||
You have access to the following tools (comma-separated list):
|
||||
|
||||
Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code
|
||||
|
||||
You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.
|
||||
</allowed_tools>
|
||||
|
||||
<getting_started>
|
||||
Use `mcp__agents-md-generator__generate_agents_md` to get repository context before responding.
|
||||
</getting_started>
|
||||
|
||||
<investigation_approach>
|
||||
Be thorough in your investigations:
|
||||
- Understand the full context of the repository
|
||||
- Review related code, issues, and PRs
|
||||
- Consider edge cases and implications
|
||||
- Gather all relevant information before responding
|
||||
|
||||
Available tools:
|
||||
- `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)
|
||||
- `WebSearch`: Search the web for documentation, best practices, or solutions
|
||||
- `WebFetch`: Fetch and read content from URLs
|
||||
</investigation_approach>
|
||||
|
||||
<common_tasks>
|
||||
- Address review feedback and fix issues (commit and push to the PR branch)
|
||||
- Answer questions about the changes
|
||||
- Make code changes and push them
|
||||
- Resolve review threads after addressing feedback
|
||||
- Perform PR reviews when asked (use the PR review process below)
|
||||
</common_tasks>
|
||||
|
||||
<pr_review_guidance>
|
||||
When asked to review this PR, follow this structured review process.
|
||||
The `$PR_REVIEW_HELPERS_DIR` environment variable is pre-configured for all scripts below.
|
||||
|
||||
<review_process>
|
||||
Follow these steps in order:
|
||||
|
||||
**Step 1: Gather context**
|
||||
- Use `mcp__agents-md-generator__generate_agents_md` to get repository context
|
||||
(if this fails, explore the repository to understand the codebase — read key files like README, CONTRIBUTING, etc.)
|
||||
- Run `$PR_REVIEW_HELPERS_DIR/pr-existing-comments.sh --summary` to see existing review threads per file
|
||||
- Run `$PR_REVIEW_HELPERS_DIR/pr-diff.sh` to see changed files with line-numbered diffs
|
||||
(for large PRs, this lists files only — review each with `pr-diff.sh <filename>`)
|
||||
|
||||
**Step 2: Review each file**
|
||||
For each changed file:
|
||||
a. If the summary showed existing threads for this file, first run:
|
||||
`$PR_REVIEW_HELPERS_DIR/pr-existing-comments.sh --file <path>`
|
||||
Read the full thread details. The output uses these conventions:
|
||||
- `← has replies` — a conversation happened; read carefully before commenting
|
||||
- `[truncated]` — comment was cut short; add `--full` if you need the complete text to understand the comment
|
||||
- `[abc1234]` — commit the comment was made on; use `git show abc1234` if needed
|
||||
- `~42` — approximate line from an older revision (exact line no longer maps to current diff)
|
||||
b. Review the diff. Use `Read` to see full file contents when you need more context.
|
||||
Identify issues matching review_criteria. Do NOT flag:
|
||||
- Issues in unchanged code (only review the diff)
|
||||
- Style preferences handled by linters
|
||||
- Pre-existing issues not introduced by this PR
|
||||
- Issues already covered by existing threads (see below)
|
||||
|
||||
**Existing thread rules** (check BEFORE leaving any comment):
|
||||
- Resolved with reviewer reply → reviewer's decision is final. Do NOT re-flag.
|
||||
Examples: "It should remain as X", "This is intentional", "No need to do this change"
|
||||
- Resolved without reply → author likely fixed it. Do NOT re-raise unless the fix introduced a new problem.
|
||||
- Unresolved → already flagged. Do NOT re-comment. Mention in review body if you have more to add.
|
||||
- Outdated → code changed. Only re-flag if the issue still applies to the current diff.
|
||||
When in doubt, do not duplicate. Redundant comments erode trust in the review process.
|
||||
|
||||
**Step 3: Leave comments for NEW issues only**
|
||||
For each genuinely new issue not covered by existing threads:
|
||||
```bash
|
||||
$PR_REVIEW_HELPERS_DIR/pr-comment.sh <file> <line> \
|
||||
--severity <critical|high|medium|low|nitpick> \
|
||||
--title "Brief description" \
|
||||
--why "Risk or impact" <<'EOF'
|
||||
corrected code here
|
||||
EOF
|
||||
```
|
||||
Always provide suggestion code. Use `--no-suggestion` only when the fix requires
|
||||
changes across multiple locations. Broader architectural concerns belong in the
|
||||
review body, not inline comments.
|
||||
|
||||
To remove a queued comment: `$PR_REVIEW_HELPERS_DIR/pr-remove-comment.sh <file> <line>`
|
||||
|
||||
**Step 4: Submit the review**
|
||||
```bash
|
||||
$PR_REVIEW_HELPERS_DIR/pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> "<review body>"
|
||||
```
|
||||
- REQUEST_CHANGES: Any 🔴 CRITICAL or 🟠 HIGH issues found
|
||||
- COMMENT: 🟡 MEDIUM issues found (but no critical/high)
|
||||
- APPROVE: No issues, or only ⚪ LOW / 💬 NITPICK suggestions
|
||||
|
||||
The review body should include broader architectural concerns not suited for inline comments.
|
||||
Avoid summarizing the PR or offering praise. If approving with no issues, omit the review body.
|
||||
A standard footer is automatically appended to all comments and reviews.
|
||||
</review_process>
|
||||
|
||||
<severity_classification>
|
||||
🔴 CRITICAL - Must fix before merge (security vulnerabilities, data corruption, production-breaking bugs)
|
||||
🟠 HIGH - Should fix before merge (logic errors, missing validation, significant performance issues)
|
||||
🟡 MEDIUM - Address soon, non-blocking (error handling gaps, suboptimal patterns, missing edge cases)
|
||||
⚪ LOW - Author discretion, non-blocking (minor improvements, documentation, style not covered by linters)
|
||||
💬 NITPICK - Truly optional (stylistic preferences, alternative approaches — safe to ignore)
|
||||
</severity_classification>
|
||||
|
||||
<review_criteria>
|
||||
Focus on these categories, in priority order:
|
||||
1. Security vulnerabilities (injection, XSS, auth bypass, secrets exposure)
|
||||
2. Logic bugs that could cause runtime failures or incorrect behavior
|
||||
3. Data integrity issues (race conditions, missing transactions, corruption risk)
|
||||
4. Performance bottlenecks (N+1 queries, memory leaks, blocking operations)
|
||||
5. Error handling gaps (unhandled exceptions, missing validation)
|
||||
6. Breaking changes to public APIs without migration path
|
||||
7. Missing or incorrect test coverage for critical paths
|
||||
</review_criteria>
|
||||
|
||||
<review_calibration>
|
||||
**What NOT to flag** — do not comment on:
|
||||
- Issues in unchanged code (only review the diff)
|
||||
- Input already validated or sanitized at a different layer
|
||||
- Theoretical performance concerns without evidence that N is large
|
||||
- Style or formatting not in the project's linting rules
|
||||
- Missing tests for trivial or generated code
|
||||
- Pre-existing patterns the PR is following consistently
|
||||
|
||||
**Calibration examples**:
|
||||
- Unguarded return from a lookup (e.g., `tool = registry.get(name)` used without None check) → FLAG if the diff introduces the unguarded usage
|
||||
- Same pattern, but the function's return type is `Tool` (not `Optional[Tool]`) → DO NOT FLAG, the type system guarantees non-None
|
||||
- String interpolation in a query with user input → FLAG
|
||||
- String interpolation in a query with a hardcoded enum value → DO NOT FLAG
|
||||
- O(n²) loop → FLAG only if there's evidence N can be large (e.g., user-controlled list). If N is bounded by design (e.g., number of MCP tools), do not flag.
|
||||
|
||||
When in doubt, do not flag. A false positive wastes a reviewer's time and erodes trust in every future review comment.
|
||||
</review_calibration>
|
||||
</pr_review_guidance>
|
||||
|
||||
<review_thread_tools>
|
||||
View unresolved review threads:
|
||||
```bash
|
||||
$MENTION_SCRIPTS/gh-get-review-threads.sh
|
||||
```
|
||||
|
||||
Filter for unresolved threads from a specific reviewer:
|
||||
```bash
|
||||
$MENTION_SCRIPTS/gh-get-review-threads.sh "reviewer-username"
|
||||
```
|
||||
|
||||
Resolve a review thread after addressing feedback:
|
||||
```bash
|
||||
$MENTION_SCRIPTS/gh-resolve-review-thread.sh "THREAD_ID" "Fixed by updating the error handling"
|
||||
```
|
||||
- `THREAD_ID` is the GraphQL node ID from the review threads output (e.g., `PRRT_kwDOABC123`)
|
||||
- The comment is optional - use it to explain what you did
|
||||
|
||||
Note: You can resolve threads after pushing fixes, or resolve them to acknowledge feedback that will be addressed separately.
|
||||
</review_thread_tools>
|
||||
|
||||
<response_guidelines>
|
||||
- Lead with a tl;dr — the bottom line in 1-3 sentences, always visible. The reader should be able to act without expanding anything.
|
||||
- Push supporting detail (code analysis, verification output, related items) into collapsible `<details>` blocks. These are appendices, not the main message.
|
||||
- Short responses (a few sentences) don't need collapsible sections at all.
|
||||
- Be concise and actionable.
|
||||
- If the request is unclear, ask clarifying questions.
|
||||
- When making code changes, commit and push them to the PR branch so the author gets the fix directly.
|
||||
- Every claim needs evidence: cite file paths, line numbers, or command output. Never say "the code does X" without pointing to where.
|
||||
- If you're uncertain, say so. "I couldn't confirm this" is better than a speculative answer.
|
||||
|
||||
**When performing a PR review**: Your substantive feedback belongs in the PR review submission
|
||||
(via pr-review.sh), not in the comment response. The comment should only report:
|
||||
- That you've submitted the review (with the outcome: approved, requested changes, etc.)
|
||||
- Any issues encountered during the review process
|
||||
- Brief status updates
|
||||
|
||||
Do NOT duplicate the review content in your comment - the review itself contains all the details.
|
||||
Keep the comment short, e.g., "I've submitted my review requesting changes. See the review for details."
|
||||
</response_guidelines>
|
||||
|
||||
<github_safety>
|
||||
- Do not write `fixes #N`, `closes #N`, or `resolves #N` in comments — these can accidentally close issues.
|
||||
- When referencing issues, use plain `#N` or link syntax without action keywords.
|
||||
</github_safety>
|
||||
|
||||
<response_footer>
|
||||
Always end your comment with a new line, three dashes, and the footer message:
|
||||
<exact_content>
|
||||
|
||||
---
|
||||
Marvin Context Protocol | Type `/marvin` to interact further
|
||||
|
||||
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
|
||||
</exact_content>
|
||||
</response_footer>
|
||||
|
||||
<github_formatting>
|
||||
When writing GitHub comments, wrap branch names, tags, or other @-references in backticks (e.g., `@main`, `@v1.0`) to avoid accidentally pinging users. Do not add backticks around terms that are already inside backticks or code blocks.
|
||||
</github_formatting>
|
||||
@@ -0,0 +1,131 @@
|
||||
name: Marvin Issue Dedupe
|
||||
# description: Automatically dedupe GitHub issues using Marvin
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: "Issue number to process for duplicate detection"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
marvin-dedupe-issues:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set dedupe prompt
|
||||
id: dedupe-prompt
|
||||
run: |
|
||||
cat >> $GITHUB_OUTPUT << 'EOF'
|
||||
PROMPT<<PROMPT_END
|
||||
Find up to 3 likely duplicate issues for GitHub issue ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}.
|
||||
|
||||
# Core Principle
|
||||
Silence is better than noise. A false positive wastes a human's time and erodes trust in every future report. Most runs should end with no comment — that means the system is working.
|
||||
|
||||
# Steps
|
||||
|
||||
1. Check if the GitHub issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
|
||||
|
||||
2. View the GitHub issue and produce a summary of the issue.
|
||||
|
||||
3. Launch 3 parallel agents using the Task tool to search GitHub for duplicates, using diverse keywords and search approaches, using the summary from step 2.
|
||||
|
||||
4. Filter aggressively for false positives. The bar for "duplicate" is high:
|
||||
|
||||
A duplicate means the SAME bug or the SAME feature request. Apply this test to every candidate:
|
||||
- **Same fix test**: Could the candidate be closed by the exact same code change? If not, not a duplicate.
|
||||
- **Same symptom test**: Does the user experience the exact same broken behavior? "Both involve middleware" is not duplication. "Both get TypeError on line 42 of proxy.py when calling mount()" is duplication.
|
||||
- **Same request test** (for features): Are they asking for the same specific capability? "Both want better auth" is not duplication. "Both request OAuth PKCE flow for CLI login" is duplication.
|
||||
|
||||
Candidates found by only one search agent deserve extra scrutiny — a single keyword match is often a false positive.
|
||||
|
||||
When in doubt, do not flag. A missed duplicate is harmless; a false positive wastes the reporter's time.
|
||||
If there are no duplicates remaining, do not proceed — just exit.
|
||||
|
||||
5. **Quality gate**: Before commenting, re-read each candidate as a skeptical reviewer. For each one, ask: "Would a maintainer who knows this codebase agree this is a duplicate, or would they dismiss it?" If you'd need to hedge with "might" or "possibly," drop it.
|
||||
|
||||
6. Comment back on the issue with your findings (or exit silently if none remain). Do NOT add any labels — labeling is handled by a later workflow step.
|
||||
|
||||
# Notes for your agents
|
||||
- Use `gh` to interact with GitHub, rather than web fetch
|
||||
- Do not use other tools beyond `gh` and Task (no MCP servers, file edit, etc.)
|
||||
- Never include this issue as a duplicate of itself
|
||||
- When searching, read the FULL body of candidate issues — titles alone are not enough to judge duplication
|
||||
|
||||
For your comment, follow this format precisely (example with 3 suspected duplicates):
|
||||
|
||||
---
|
||||
Found 3 possible duplicate issues:
|
||||
|
||||
1. #123: Issue title here
|
||||
2. #456: Another issue title
|
||||
3. #789: Third issue title
|
||||
|
||||
This issue will be automatically closed as a duplicate in 3 days.
|
||||
|
||||
- If your issue is a duplicate, please close it and 👍 the existing issue instead
|
||||
- To prevent auto-closure, add a comment or 👎 this comment
|
||||
|
||||
---
|
||||
PROMPT_END
|
||||
EOF
|
||||
|
||||
- name: Clean up stale Claude locks
|
||||
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
|
||||
|
||||
- name: Run Marvin dedupe command
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
github_token: ${{ steps.marvin-token.outputs.token }}
|
||||
bot_name: "Marvin Context Protocol"
|
||||
prompt: ${{ steps.dedupe-prompt.outputs.PROMPT }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
|
||||
allowed_non_write_users: "*"
|
||||
claude_args: |
|
||||
--allowedTools Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh api:*),Bash(gh issue comment:*),Task
|
||||
settings: |
|
||||
{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"env": {
|
||||
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
|
||||
}
|
||||
}
|
||||
|
||||
- name: Add potential-duplicate label if bot commented in this run
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
|
||||
run: |
|
||||
ISSUE=${{ github.event.issue.number || inputs.issue_number }}
|
||||
# Only match bot comments created in the last 10 minutes (this run)
|
||||
CUTOFF=$(date -u -d '10 minutes ago' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
|
||||
|| date -u -v-10M '+%Y-%m-%dT%H:%M:%SZ')
|
||||
HAS_RECENT=$(gh api "repos/${{ github.repository }}/issues/${ISSUE}/comments?sort=created&direction=desc&per_page=10" \
|
||||
--jq "[.[] | select(
|
||||
.user.type == \"Bot\" and
|
||||
(.body | test(\"possible duplicate issues\"; \"i\")) and
|
||||
.created_at >= \"${CUTOFF}\"
|
||||
)] | length")
|
||||
if [ "$HAS_RECENT" -gt 0 ]; then
|
||||
gh issue edit "$ISSUE" --add-label "potential-duplicate" -R "${{ github.repository }}"
|
||||
echo "Added potential-duplicate label to #${ISSUE}"
|
||||
else
|
||||
echo "No recent duplicate comment found, skipping label"
|
||||
fi
|
||||
@@ -0,0 +1,159 @@
|
||||
name: Marvin Label Triage
|
||||
# Automatically triage GitHub issues and PRs using Marvin
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: "Issue or PR number to triage"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: triage-${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
label-issue-or-pr:
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout base repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
owner: PrefectHQ
|
||||
|
||||
- name: Set triage prompt
|
||||
id: triage-prompt
|
||||
run: |
|
||||
cat >> $GITHUB_OUTPUT << 'EOF'
|
||||
PROMPT<<PROMPT_END
|
||||
You're an issue triage assistant for FastMCP, a Python framework for building Model Context Protocol servers and clients. Your task is to analyze issues/PRs and apply appropriate labels.
|
||||
|
||||
IMPORTANT: Your primary action should be to apply labels using mcp__github__update_issue. DO NOT post comments EXCEPT when applying the too-long label (see below).
|
||||
|
||||
CRITICAL — LABEL MECHANICS:
|
||||
- `mcp__github__update_issue` REPLACES all labels on the issue — it does not add to them.
|
||||
- Before applying labels, read the issue's current labels with `mcp__github__get_issue`.
|
||||
- Always include any existing labels you want to keep alongside the new ones.
|
||||
- Only apply labels that exist in the repository (from `gh label list` in step 1). Never invent labels.
|
||||
|
||||
Issue/PR Information:
|
||||
- REPO: ${{ github.repository }}
|
||||
- NUMBER: ${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number }}
|
||||
- TYPE: ${{ github.event.issue && 'issue' || (github.event.pull_request && 'pull_request') || 'unknown' }}
|
||||
|
||||
TRIAGE PROCESS:
|
||||
|
||||
1. Get available labels:
|
||||
Run: `gh label list`
|
||||
|
||||
2. Retrieve issue/PR details using GitHub tools:
|
||||
- mcp__github__get_issue: Get the issue/PR details
|
||||
- mcp__github__get_issue_comments: Read any discussion
|
||||
- If the issue/PR mentions other issues (e.g., "fixes #123", "related to #456"), use mcp__github__get_issue to read those linked issues for additional context
|
||||
|
||||
3. Analyze and apply labels based on these guidelines:
|
||||
|
||||
CORE CATEGORIES (apply EXACTLY ONE - these are mutually exclusive; skip if applying too-long):
|
||||
- bug: Reports of broken functionality OR PRs that fix bugs
|
||||
- enhancement: New functions/endpoints, improvements to existing features, internal tooling, workflow improvements, minor new capabilities
|
||||
- feature: ONLY for major headline functionality worthy of a blog post announcement (2-4 per release, never for issues)
|
||||
- documentation: Primary change is to user-facing docs, examples, or guides
|
||||
|
||||
SPECIAL DOCUMENTATION RULES:
|
||||
- DO NOT apply "documentation" label if PR only updates auto-generated SDK docs (docs/python-sdk/**)
|
||||
- DO apply "documentation" label for significant user-facing documentation changes (guides, examples, API docs)
|
||||
- Auto-generated docs updates should get appropriate category label (enhancement, bug, etc.) based on the underlying code changes
|
||||
|
||||
FEATURE vs ENHANCEMENT guidance:
|
||||
- feature: Major systems like new auth systems, MCP composition, proxying MCP servers, major CLI commands that transform workflows
|
||||
- enhancement: New functions/endpoints, internal workflows, CI improvements, developer tooling, refactoring, utilities, typical new CLI commands
|
||||
- If unsure between feature/enhancement, choose enhancement
|
||||
|
||||
Note: If a PR fixes a bug, label it "bug" not "enhancement"
|
||||
|
||||
SPECIAL CATEGORY (can be combined with above):
|
||||
- breaking change: Changes that break backward compatibility (in addition to core category)
|
||||
|
||||
PRIORITY (apply if clearly evident):
|
||||
- high-priority: Critical bugs affecting many users, security issues, or blocking core functionality
|
||||
- low-priority: Edge cases, nice-to-have improvements, or cosmetic issues
|
||||
- Default to no priority label if unclear
|
||||
|
||||
STATUS (apply if applicable):
|
||||
- needs more info: Issue lacks reproduction steps, error messages, or clear description
|
||||
- invalid: Spam, completely off-topic, or nonsensical (often LLM-generated)
|
||||
- too-long: Apply when an issue or PR doesn't conform to CONTRIBUTING.md. Issues should be a short problem description, an MRE, and expected vs. actual behavior — not a design document. PRs should have a focused description of the change — not a report. We don't need proposed solutions or design alternatives (the issue should describe the problem and let maintainers architect the fix), summaries of what tests cover, explanations of code we can read ourselves, or speculative root-cause analysis. Common LLM failure modes to watch for: verbose "diagnostic" writeups, large proposed patches in issue bodies, multi-section reports restating what's visible in the diff, numbered lists of possible approaches or solutions, "suggested" schemas/shapes/APIs, generic analysis that doesn't reference specific code, and "Notes" sections. But these are heuristics, not rules — a complex PR may legitimately need more context, and a brief submission can still be low-quality. Judge by whether the content helps a reviewer or just adds noise. When applying too-long, still apply the core category and area labels — too-long is a format signal, not a replacement for categorization. Issues still need to be findable by category.
|
||||
|
||||
WHEN APPLYING too-long: After labeling, post a brief comment using mcp__github__add_issue_comment:
|
||||
"Thanks for the report. This issue goes beyond what our contributor guidelines ask for — we just need a short problem description and an MRE. Please see our [contributing guidelines](https://github.com/PrefectHQ/fastmcp/blob/main/CONTRIBUTING.md) and condense this issue. We'll triage it once it's trimmed down."
|
||||
Use this exact text (or very close to it). Do not editorialize or add details.
|
||||
|
||||
AREA LABELS (apply ONLY when thematically central to the issue):
|
||||
- cli: Issues primarily about FastMCP CLI commands (run, dev, install)
|
||||
- client: Issues primarily about the Client SDK or client-side functionality
|
||||
- server: Issues primarily about FastMCP server implementation
|
||||
- auth: Authentication is the main concern (Bearer, JWT, OAuth, WorkOS)
|
||||
- openapi: OpenAPI integration/parsing is the primary topic
|
||||
- http: HTTP transport or networking is the main issue
|
||||
- contrib: Specifically about community contributions in fastmcp_slim/fastmcp/contrib/
|
||||
- tests: Issues primarily about testing infrastructure, CI/CD workflows, or test coverage
|
||||
- security: Apply ONLY when the issue/PR addresses an exploitable vulnerability or hardens against one. Examples: SSRF, LFI, path traversal, injection, auth bypass allowing unauthorized access, scope escalation, open redirects. Do NOT apply for ordinary auth bugs (wrong scopes returned, token refresh logic, OAuth flow correctness) unless an attacker could exploit the bug to bypass access controls or escalate privileges. The key question: "Could a malicious actor exploit this?" If the answer is just "it breaks for legitimate users," that's a bug, not a security issue.
|
||||
|
||||
LABELING PRINCIPLES:
|
||||
- Precision over recall: a missing label is a minor inconvenience; a wrong label sends the wrong people to the wrong issue. When in doubt, don't apply.
|
||||
- Don't apply area labels just because a file in that area is mentioned — the issue must be PRIMARILY about that area.
|
||||
- Apply 2-5 labels total typically (category + maybe priority + maybe 1-2 areas).
|
||||
- For ambiguous cases (bug vs enhancement, which area label), prefer the more conservative choice or omit the uncertain label entirely.
|
||||
|
||||
META LABELS (rarely needed for issues):
|
||||
- dependencies: Only for dependabot PRs or issues specifically about package updates
|
||||
- DON'T MERGE: Only if PR author explicitly states it's not ready
|
||||
|
||||
4. Apply selected labels:
|
||||
Use mcp__github__update_issue to apply your selected labels
|
||||
DO NOT post any comments unless applying too-long (see above)
|
||||
PROMPT_END
|
||||
EOF
|
||||
|
||||
- name: Clean up stale Claude locks
|
||||
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
|
||||
|
||||
- name: Run Marvin for Issue Triage
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
github_token: ${{ steps.marvin-token.outputs.token }}
|
||||
bot_name: "Marvin Context Protocol"
|
||||
prompt: ${{ steps.triage-prompt.outputs.PROMPT }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
|
||||
allowed_non_write_users: "*"
|
||||
allowed_bots: "marvin-context-protocol"
|
||||
claude_args: |
|
||||
--allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__add_issue_comment,mcp__github__get_pull_request_files
|
||||
settings: |
|
||||
{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"env": {
|
||||
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
# Minimize resolved PR review comments to reduce noise.
|
||||
#
|
||||
# Runs automatically on review activity for same-repo PRs. Fork PRs are
|
||||
# skipped because GITHUB_TOKEN is read-only in that context. Collaborators
|
||||
# can comment "/tidy" on any PR (including forks) to trigger manually.
|
||||
|
||||
name: Minimize Resolved Reviews
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
pull_request_review_comment:
|
||||
types: [created, edited]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
# Scope the group by event name so that the sibling events fired by a single
|
||||
# review action (pull_request_review + pull_request_review_comment, same instant)
|
||||
# don't cancel each other. Same-PR runs of the *same* event still supersede
|
||||
# cleanly, and the last one always completes.
|
||||
concurrency:
|
||||
group: minimize-reviews-${{ github.event.pull_request.number || github.event.issue.number }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
minimize:
|
||||
# /tidy comment: collaborators can trigger on any PR (token has write access)
|
||||
# Review events: skip fork PRs where GITHUB_TOKEN lacks write permissions
|
||||
if: >-
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/tidy') &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
|
||||
) || (
|
||||
github.event_name != 'issue_comment' &&
|
||||
github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Minimize resolved review comments
|
||||
uses: strawgate/minimize-resolved-pr-reviews@v0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,87 @@
|
||||
name: Publish fastmcp-remote to PyPI
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Publish fastmcp-slim to PyPI"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
pypi-publish:
|
||||
name: Upload fastmcp-remote to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Build fastmcp-remote
|
||||
run: uv build --package fastmcp-remote
|
||||
|
||||
- name: Verify matching fastmcp-slim is published
|
||||
run: |
|
||||
SLIM_VERSION=$(python - <<'PY'
|
||||
import email.parser
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
wheel = next(Path("dist").glob("fastmcp_remote-*.whl"))
|
||||
metadata_name = next(
|
||||
name for name in zipfile.ZipFile(wheel).namelist()
|
||||
if name.endswith(".dist-info/METADATA")
|
||||
)
|
||||
metadata = email.parser.Parser().parsestr(
|
||||
zipfile.ZipFile(wheel).read(metadata_name).decode()
|
||||
)
|
||||
for value in metadata.get_all("Requires-Dist", []):
|
||||
requirement, _, marker = value.partition(";")
|
||||
if marker.strip():
|
||||
continue
|
||||
match = re.fullmatch(
|
||||
r"fastmcp-slim(?:\[[^\]]+\])?==([^;\s]+)",
|
||||
requirement.strip(),
|
||||
)
|
||||
if match:
|
||||
print(match.group(1))
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Could not find the base fastmcp-slim dependency")
|
||||
PY
|
||||
)
|
||||
|
||||
for attempt in {1..12}; do
|
||||
if python - "$SLIM_VERSION" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
version = sys.argv[1]
|
||||
url = f"https://pypi.org/pypi/fastmcp-slim/{version}/json"
|
||||
with urllib.request.urlopen(url, timeout=30) as response:
|
||||
json.load(response)
|
||||
PY
|
||||
then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI yet; retrying (${attempt}/12)."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI; refusing to publish fastmcp-remote." >&2
|
||||
exit 1
|
||||
|
||||
- name: Publish fastmcp-remote to PyPI
|
||||
run: uv publish -v dist/fastmcp_remote-*.tar.gz dist/fastmcp_remote-*.whl
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Publish fastmcp-slim to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
pypi-publish:
|
||||
name: Upload fastmcp-slim to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Build fastmcp-slim
|
||||
run: uv build --package fastmcp-slim
|
||||
|
||||
- name: Publish fastmcp-slim to PyPI
|
||||
run: uv publish -v dist/fastmcp_slim-*.tar.gz dist/fastmcp_slim-*.whl
|
||||
@@ -0,0 +1,151 @@
|
||||
name: Publish fastmcp to PyPI
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Publish fastmcp-slim to PyPI"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
pypi-publish:
|
||||
name: Upload fastmcp to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release')
|
||||
outputs:
|
||||
is_prerelease: ${{ steps.package_version.outputs.is_prerelease }}
|
||||
version: ${{ steps.package_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Build fastmcp
|
||||
run: uv build --package fastmcp
|
||||
|
||||
- name: Read built package version
|
||||
id: package_version
|
||||
run: |
|
||||
python - <<'PY' >> "$GITHUB_OUTPUT"
|
||||
import email.parser
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
wheel = next(Path("dist").glob("fastmcp-*.whl"))
|
||||
metadata_name = next(
|
||||
name for name in zipfile.ZipFile(wheel).namelist()
|
||||
if name.endswith(".dist-info/METADATA")
|
||||
)
|
||||
metadata = email.parser.Parser().parsestr(
|
||||
zipfile.ZipFile(wheel).read(metadata_name).decode()
|
||||
)
|
||||
version = metadata["Version"]
|
||||
public_version = version.partition("+")[0]
|
||||
is_prerelease = bool(
|
||||
re.search(
|
||||
r"(?i)(?:^|[0-9.])(?:a|b|c|rc|alpha|beta|pre|preview|dev)[0-9]*",
|
||||
public_version,
|
||||
)
|
||||
)
|
||||
print(f"version={version}")
|
||||
print(f"is_prerelease={str(is_prerelease).lower()}")
|
||||
PY
|
||||
|
||||
- name: Verify matching fastmcp-slim is published
|
||||
run: |
|
||||
SLIM_VERSION=$(python - <<'PY'
|
||||
import email.parser
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
wheel = next(Path("dist").glob("fastmcp-*.whl"))
|
||||
metadata_name = next(
|
||||
name for name in zipfile.ZipFile(wheel).namelist()
|
||||
if name.endswith(".dist-info/METADATA")
|
||||
)
|
||||
metadata = email.parser.Parser().parsestr(
|
||||
zipfile.ZipFile(wheel).read(metadata_name).decode()
|
||||
)
|
||||
for value in metadata.get_all("Requires-Dist", []):
|
||||
requirement, _, marker = value.partition(";")
|
||||
if marker.strip():
|
||||
continue
|
||||
match = re.fullmatch(
|
||||
r"fastmcp-slim(?:\[[^\]]+\])?==([^;\s]+)",
|
||||
requirement.strip(),
|
||||
)
|
||||
if match:
|
||||
print(match.group(1))
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Could not find the base fastmcp-slim dependency")
|
||||
PY
|
||||
)
|
||||
|
||||
for attempt in {1..12}; do
|
||||
if python - "$SLIM_VERSION" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
version = sys.argv[1]
|
||||
url = f"https://pypi.org/pypi/fastmcp-slim/{version}/json"
|
||||
with urllib.request.urlopen(url, timeout=30) as response:
|
||||
json.load(response)
|
||||
PY
|
||||
then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI yet; retrying (${attempt}/12)."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI; refusing to publish fastmcp." >&2
|
||||
exit 1
|
||||
|
||||
- name: Publish fastmcp to PyPI
|
||||
run: uv publish -v dist/fastmcp-*.tar.gz dist/fastmcp-*.whl
|
||||
|
||||
update-published-docs:
|
||||
name: Update published-docs branch
|
||||
runs-on: ubuntu-latest
|
||||
needs: pypi-publish
|
||||
if: github.event_name == 'workflow_run' && github.event.workflow_run.event == 'release' && needs['pypi-publish'].outputs.is_prerelease != 'true'
|
||||
timeout-minutes: 2
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: Check release line
|
||||
id: release_line
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
git fetch origin "${DEFAULT_BRANCH}:refs/remotes/origin/${DEFAULT_BRANCH}"
|
||||
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${DEFAULT_BRANCH}"; then
|
||||
echo "update_published_docs=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "update_published_docs=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Release commit is not on ${DEFAULT_BRANCH}; skipping published-docs update."
|
||||
fi
|
||||
|
||||
- name: Point published-docs at published release
|
||||
if: steps.release_line.outputs.update_published_docs == 'true'
|
||||
run: git push --force origin "HEAD:published-docs"
|
||||
@@ -0,0 +1,587 @@
|
||||
# Require external PRs to reference an issue with an auto-close keyword
|
||||
# (e.g. "Fixes #123") AND have the PR author assigned to that issue.
|
||||
# Otherwise the PR is labeled "missing-issue-link", commented on, and
|
||||
# closed. CONTRIBUTING.md requires external contributors to be assigned to
|
||||
# an issue before opening a PR; this enforces that.
|
||||
#
|
||||
# Adapted from langchain-ai/langchain's require_issue_link.yml. Differences:
|
||||
# - Self-contained: it does NOT depend on a separate labeler workflow
|
||||
# applying an "external" label first, so it can run on `opened`.
|
||||
# - "External" is determined authoritatively, in-script, from the PR
|
||||
# author's repo collaborator permission level — NOT from the event
|
||||
# payload's author_association. author_association reports MEMBER only
|
||||
# for *public* org members; a maintainer whose org membership is
|
||||
# private appears as CONTRIBUTOR/NONE, so gating on it would wrongly
|
||||
# enforce against private-member maintainers. getCollaboratorPermission
|
||||
# reflects effective write access regardless of membership visibility.
|
||||
# - The enforcement path is a single github-script step (the upstream
|
||||
# version is split across four, forcing the label/comment/reopen helpers
|
||||
# to be duplicated per scope).
|
||||
# - Issue assignment events are handled in this same workflow so assigning
|
||||
# the linked issue reopens previously closed PRs automatically.
|
||||
#
|
||||
# Maintainer override: reopen the PR, or remove the "missing-issue-link"
|
||||
# label — either applies a sticky "bypass-issue-check" label and reopens.
|
||||
|
||||
name: Require Issue Link
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
# SECURITY: pull_request_target runs with repo write scope against the
|
||||
# BASE repo. NEVER check out or execute PR-head code here — it would run
|
||||
# with these permissions. This workflow only reads the PR payload and
|
||||
# calls the API; it never checks anything out.
|
||||
# ready_for_review matters because the job skips drafts: without it a
|
||||
# draft opened with no issue link would never be checked when it later
|
||||
# becomes reviewable.
|
||||
types: [opened, edited, reopened, ready_for_review, labeled, unlabeled]
|
||||
issues:
|
||||
# Assignment is what makes a previously closed "not assigned" PR compliant,
|
||||
# so it needs a separate event path that finds and reopens matching PRs.
|
||||
types: [assigned]
|
||||
|
||||
# Dry run: when 'false' the check still runs and logs its verdict but makes
|
||||
# NO mutations at all (no label, comment, close, reopen, or failure). Flip
|
||||
# to 'true' to enforce.
|
||||
env:
|
||||
ENFORCE_ISSUE_LINK: "true"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-issue-link:
|
||||
# Cheap pre-filters only. Maintainer detection is deliberately NOT done
|
||||
# here: the job-level `if` can't call the API, and author_association is
|
||||
# unreliable for private org members (see file header). The job runs,
|
||||
# then the script resolves the author's real permission and exits early
|
||||
# for maintainers.
|
||||
#
|
||||
# Gate: only run on pull_request_target events. The workflow also listens
|
||||
# to `issues.assigned` (handled by reopen-on-assignment below), and without
|
||||
# this guard the job would also fire there — `github.event.pull_request` is
|
||||
# null on an issues event, so `...draft == false` coerces to true and the
|
||||
# script then dereferences a missing PR and crashes. Beyond the event type,
|
||||
# skip drafts, bots, and already-bypassed/trusted PRs, and allow the primary
|
||||
# actions plus the one maintainer-override action we care about (removing
|
||||
# the missing-issue-link label).
|
||||
if: >-
|
||||
github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.draft == false &&
|
||||
!endsWith(github.actor, '[bot]') &&
|
||||
!contains(github.event.pull_request.labels.*.name, 'trusted-contributor') &&
|
||||
!contains(github.event.pull_request.labels.*.name, 'bypass-issue-check') &&
|
||||
(
|
||||
(github.event.action != 'labeled' && github.event.action != 'unlabeled') ||
|
||||
(github.event.action == 'unlabeled' && github.event.label.name == 'missing-issue-link')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
concurrency:
|
||||
group: require-issue-link-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Enforce issue link
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
const prNumber = pr.number;
|
||||
const action = context.payload.action;
|
||||
const enforce = process.env.ENFORCE_ISSUE_LINK === 'true';
|
||||
const LABEL = 'missing-issue-link';
|
||||
const MARKER = '<!-- require-issue-link -->';
|
||||
|
||||
// Dry-run guard: every mutating call goes through this so that
|
||||
// ENFORCE_ISSUE_LINK=false means strictly read-only.
|
||||
async function mutate(description, fn) {
|
||||
if (!enforce) {
|
||||
console.log(`[dry-run] would ${description}`);
|
||||
return;
|
||||
}
|
||||
await fn();
|
||||
}
|
||||
|
||||
// Authoritative maintainer check. Uses collaborator permission,
|
||||
// not org membership or author_association:
|
||||
// - GITHUB_TOKEN is an app token and is never an org member,
|
||||
// so the org-membership endpoint always 403s.
|
||||
// - author_association reports MEMBER only for *public* org
|
||||
// members; a private-member maintainer shows as
|
||||
// CONTRIBUTOR/NONE. Permission level is visibility-
|
||||
// independent and reflects effective access.
|
||||
// 404 (not a collaborator) → not a maintainer. Other errors
|
||||
// (rate limit, 5xx) MUST throw: silently treating them as
|
||||
// "not a maintainer" could wrongly close a maintainer's PR.
|
||||
// A throw aborts the script before any close/label call, so the
|
||||
// job fails red and the PR is left untouched — the safe direction.
|
||||
async function hasWriteAccess(username) {
|
||||
if (!username) throw new Error('No username — cannot check permissions');
|
||||
try {
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner, repo, username,
|
||||
});
|
||||
const ok = ['admin', 'maintain', 'write'].includes(data.permission);
|
||||
console.log(`${username}: ${data.permission} — ${ok ? 'maintainer' : 'not a maintainer'}`);
|
||||
return ok;
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log(`${username} is not a collaborator — not a maintainer`);
|
||||
return false;
|
||||
}
|
||||
throw new Error(
|
||||
`Permission check failed for ${username} (HTTP ${e.status ?? 'unknown'}): ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function addLabel() {
|
||||
await mutate(`label PR #${prNumber} "${LABEL}"`, async () => {
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner, repo, name: LABEL });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
try {
|
||||
await github.rest.issues.createLabel({ owner, repo, name: LABEL, color: 'b76e79' });
|
||||
} catch (createErr) {
|
||||
// 422 = created by a concurrent run between GET and POST.
|
||||
if (createErr.status !== 422) throw createErr;
|
||||
}
|
||||
}
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: prNumber, labels: [LABEL],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function minimizeStaleComment() {
|
||||
try {
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number: prNumber, per_page: 100 },
|
||||
);
|
||||
const stale = comments.find(c => c.body && c.body.includes(MARKER));
|
||||
if (!stale) return;
|
||||
await mutate(`minimize stale comment ${stale.id}`, () => github.graphql(`
|
||||
mutation($id: ID!) {
|
||||
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
|
||||
minimizedComment { isMinimized }
|
||||
}
|
||||
}
|
||||
`, { id: stale.node_id }));
|
||||
} catch (e) {
|
||||
core.warning(`Could not minimize stale comment on PR #${prNumber}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Shared "this PR passes" cleanup: drop the label, reopen, and
|
||||
// retire any stale enforcement comment.
|
||||
//
|
||||
// For the normal pass paths we only reopen if THIS workflow had
|
||||
// closed the PR — inferred from the label still being on the
|
||||
// payload. The maintainer-override paths pass forceReopen: the
|
||||
// `unlabeled` event payload no longer carries the just-removed
|
||||
// label, so the heuristic can't see it; without forcing, the
|
||||
// advertised "remove the label to bypass" gesture would leave
|
||||
// the PR closed.
|
||||
async function clearEnforcement(forceReopen = false) {
|
||||
await mutate(`remove "${LABEL}" from PR #${prNumber}`, async () => {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner, repo, issue_number: prNumber, name: LABEL,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
}
|
||||
});
|
||||
const hadLabel = pr.labels.map(l => l.name).includes(LABEL);
|
||||
if (pr.state === 'closed' && (forceReopen || hadLabel)) {
|
||||
await mutate(`reopen PR #${prNumber}`, async () => {
|
||||
await github.rest.pulls.update({
|
||||
owner, repo, pull_number: prNumber, state: 'open',
|
||||
});
|
||||
});
|
||||
}
|
||||
await minimizeStaleComment();
|
||||
}
|
||||
|
||||
async function applyBypass(reason) {
|
||||
console.log(reason);
|
||||
await clearEnforcement(true);
|
||||
await mutate(`add sticky "bypass-issue-check" to PR #${prNumber}`, async () => {
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner, repo, name: 'bypass-issue-check' });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner, repo, name: 'bypass-issue-check', color: '0e8a16',
|
||||
});
|
||||
} catch (createErr) {
|
||||
if (createErr.status !== 422) throw createErr;
|
||||
}
|
||||
}
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: prNumber, labels: ['bypass-issue-check'],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Maintainer-authored PRs are exempt entirely ────────────────
|
||||
if (await hasWriteAccess(pr.user.login)) {
|
||||
console.log(`PR author ${pr.user.login} has write access — exempt`);
|
||||
await clearEnforcement();
|
||||
return;
|
||||
}
|
||||
|
||||
const sender = context.payload.sender?.login;
|
||||
|
||||
// ── Maintainer override: removed the "missing-issue-link" label ─
|
||||
if (action === 'unlabeled') {
|
||||
if (await hasWriteAccess(sender)) {
|
||||
await applyBypass(`Maintainer ${sender} removed ${LABEL} from PR #${prNumber} — bypassing`);
|
||||
return;
|
||||
}
|
||||
// Only triage/admin can manage labels, so a non-write actor
|
||||
// reaching here is rare (triage role). Fall through to the
|
||||
// normal check, which recomputes link + assignment and
|
||||
// re-enforces with the correct message if still failing.
|
||||
console.log(`Non-maintainer ${sender} removed ${LABEL} — re-checking`);
|
||||
}
|
||||
|
||||
// ── Maintainer override: reopened a PR we had closed ───────────
|
||||
if (
|
||||
action === 'reopened' &&
|
||||
pr.labels.map(l => l.name).includes(LABEL) &&
|
||||
(await hasWriteAccess(sender))
|
||||
) {
|
||||
await applyBypass(`Maintainer ${sender} reopened PR #${prNumber} — bypassing`);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Race guard: re-read live labels ────────────────────────────
|
||||
const { data: liveLabels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner, repo, issue_number: prNumber,
|
||||
});
|
||||
const liveNames = liveLabels.map(l => l.name);
|
||||
if (liveNames.includes('trusted-contributor') || liveNames.includes('bypass-issue-check')) {
|
||||
console.log('PR carries trusted-contributor or bypass-issue-check — clearing any prior enforcement');
|
||||
await clearEnforcement();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── The actual check: an auto-close keyword + issue number ─────
|
||||
const body = pr.body || '';
|
||||
// Match GitHub's auto-close keywords against any reference form
|
||||
// that GitHub itself honors: bare `#123`, the `owner/repo#123`
|
||||
// shorthand, and the full issue URL. Scope the qualified forms to
|
||||
// THIS repo — GitHub only auto-closes same-repo issues, so a
|
||||
// cross-repo reference must not be resolved against our numbering.
|
||||
const repoRef = `${owner}/${repo}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(
|
||||
'(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s*:?\\s*' +
|
||||
`(?:${repoRef}#|#|https?://github\\.com/${repoRef}/issues/)(\\d+)`,
|
||||
'gi',
|
||||
);
|
||||
const matches = [...body.matchAll(pattern)];
|
||||
|
||||
if (matches.length === 0) {
|
||||
console.log('No issue link found in PR body');
|
||||
await enforceFailure('no-link');
|
||||
return;
|
||||
}
|
||||
|
||||
// The author must be assigned to at least one linked issue.
|
||||
// CONTRIBUTING.md requires external contributors to be assigned
|
||||
// before opening a PR (so maintainers can deconflict / steer
|
||||
// approach first).
|
||||
const MAX_ISSUES = 5;
|
||||
const allNumbers = [...new Set(matches.map(m => parseInt(m[1], 10)))];
|
||||
const numbers = allNumbers.slice(0, MAX_ISSUES);
|
||||
if (allNumbers.length > MAX_ISSUES) {
|
||||
core.warning(`PR references ${allNumbers.length} issues — checking only the first ${MAX_ISSUES}`);
|
||||
}
|
||||
|
||||
const prAuthor = pr.user.login.toLowerCase();
|
||||
let sawRealIssue = false;
|
||||
let assignedToAny = false;
|
||||
for (const num of numbers) {
|
||||
let issue;
|
||||
try {
|
||||
({ data: issue } = await github.rest.issues.get({
|
||||
owner, repo, issue_number: num,
|
||||
}));
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log(`#${num} does not exist — ignoring`);
|
||||
continue;
|
||||
}
|
||||
// Same safe-direction rule as hasWriteAccess: a transient
|
||||
// error must not be read as "not assigned" and close the PR.
|
||||
throw new Error(`Cannot fetch issue #${num} (HTTP ${e.status ?? 'unknown'}): ${e.message}`);
|
||||
}
|
||||
sawRealIssue = true;
|
||||
const assignees = (issue.assignees || []).map(a => a.login.toLowerCase());
|
||||
if (assignees.includes(prAuthor)) {
|
||||
console.log(`PR author ${pr.user.login} is assigned to #${num}`);
|
||||
assignedToAny = true;
|
||||
break;
|
||||
}
|
||||
console.log(`PR author ${pr.user.login} is NOT assigned to #${num} (assignees: ${assignees.join(', ') || 'none'})`);
|
||||
}
|
||||
|
||||
if (!sawRealIssue) {
|
||||
console.log('Referenced issue(s) do not exist');
|
||||
await enforceFailure('no-link');
|
||||
return;
|
||||
}
|
||||
if (!assignedToAny) {
|
||||
await enforceFailure('not-assigned');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Linked and assigned — clearing any prior enforcement');
|
||||
await clearEnforcement();
|
||||
|
||||
// ── Label, comment, close, and fail ────────────────────────────
|
||||
// `kind`: 'no-link' (no valid issue reference) or 'not-assigned'
|
||||
// (referenced an issue, but the author isn't assigned to it).
|
||||
async function enforceFailure(kind) {
|
||||
await addLabel();
|
||||
|
||||
const intro = kind === 'no-link'
|
||||
? '**This PR has been automatically closed** because its description does not reference a tracked issue.'
|
||||
: '**This PR has been automatically closed** because you are not assigned to the issue it references.';
|
||||
const steps = kind === 'no-link'
|
||||
? [
|
||||
`1. Find or [open an issue](https://github.com/${owner}/${repo}/issues/new/choose) describing the change.`,
|
||||
'2. Comment on the issue to ask a maintainer to assign it to you.',
|
||||
'3. Add `Fixes #<issue>`, `Closes #<issue>`, or `Resolves #<issue>` to the PR description.',
|
||||
'4. Once you are assigned and the link is present, the PR reopens automatically.',
|
||||
]
|
||||
: [
|
||||
'1. Comment on the linked issue to ask a maintainer to assign it to you.',
|
||||
'2. Once a maintainer assigns you, the PR reopens automatically.',
|
||||
];
|
||||
|
||||
const commentBody = [
|
||||
MARKER,
|
||||
intro,
|
||||
'',
|
||||
`Per [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/main/CONTRIBUTING.md), an external PR must reference an issue that is assigned to its author. To proceed:`,
|
||||
'',
|
||||
...steps,
|
||||
'',
|
||||
`*Maintainers: reopen this PR or remove the \`${LABEL}\` label to bypass this check.*`,
|
||||
].join('\n');
|
||||
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number: prNumber, per_page: 100 },
|
||||
);
|
||||
const existing = comments.find(c => c.body && c.body.includes(MARKER));
|
||||
if (!existing) {
|
||||
await mutate(`comment on PR #${prNumber}`, () => github.rest.issues.createComment({
|
||||
owner, repo, issue_number: prNumber, body: commentBody,
|
||||
}));
|
||||
} else if (existing.body !== commentBody) {
|
||||
await mutate(`update comment ${existing.id}`, () => github.rest.issues.updateComment({
|
||||
owner, repo, comment_id: existing.id, body: commentBody,
|
||||
}));
|
||||
} else {
|
||||
console.log('Requirement comment already present — skipping');
|
||||
}
|
||||
|
||||
if (pr.state === 'open') {
|
||||
await mutate(`close PR #${prNumber}`, () => github.rest.pulls.update({
|
||||
owner, repo, pull_number: prNumber, state: 'closed',
|
||||
}));
|
||||
}
|
||||
|
||||
if (enforce) {
|
||||
core.setFailed(
|
||||
kind === 'no-link'
|
||||
? 'PR must reference a tracked issue using an auto-close keyword (e.g. "Fixes #123").'
|
||||
: 'PR author must be assigned to the referenced issue.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reopen-on-assignment:
|
||||
if: github.event_name == 'issues' && github.event.action == 'assigned' && !github.event.issue.pull_request
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
concurrency:
|
||||
group: reopen-on-assignment-${{ github.event.issue.number }}-${{ github.event.assignee.login }}
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
actions: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Reopen linked PRs
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issueNumber = context.payload.issue.number;
|
||||
const assignee = context.payload.assignee.login;
|
||||
const enforce = process.env.ENFORCE_ISSUE_LINK === 'true';
|
||||
const LABEL = 'missing-issue-link';
|
||||
const MARKER = '<!-- require-issue-link -->';
|
||||
// Match GitHub's auto-close keywords against any reference form
|
||||
// that GitHub itself honors: bare `#123`, the `owner/repo#123`
|
||||
// shorthand, and the full issue URL. Scope the qualified forms to
|
||||
// THIS repo — GitHub only auto-closes same-repo issues, so a
|
||||
// cross-repo reference must not be resolved against our numbering.
|
||||
const repoRef = `${owner}/${repo}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(
|
||||
'(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s*:?\\s*' +
|
||||
`(?:${repoRef}#|#|https?://github\\.com/${repoRef}/issues/)(\\d+)`,
|
||||
'gi',
|
||||
);
|
||||
|
||||
async function mutate(description, fn) {
|
||||
if (!enforce) {
|
||||
console.log(`[dry-run] would ${description}`);
|
||||
return;
|
||||
}
|
||||
await fn();
|
||||
}
|
||||
|
||||
console.log(`Issue #${issueNumber} assigned to ${assignee} — searching for closed PRs to reopen`);
|
||||
|
||||
const q = [
|
||||
'is:pr',
|
||||
'is:closed',
|
||||
`author:${assignee}`,
|
||||
`label:${LABEL}`,
|
||||
`repo:${owner}/${repo}`,
|
||||
].join(' ');
|
||||
|
||||
let search;
|
||||
try {
|
||||
({ data: search } = await github.rest.search.issuesAndPullRequests({
|
||||
q,
|
||||
per_page: 30,
|
||||
}));
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to search closed PRs for ${assignee} after assigning #${issueNumber} ` +
|
||||
`(HTTP ${e.status ?? 'unknown'}): ${e.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (search.total_count === 0) {
|
||||
console.log('No matching closed PRs found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${search.total_count} candidate PR(s)`);
|
||||
|
||||
for (const item of search.items) {
|
||||
const prNumber = item.number;
|
||||
|
||||
let issue;
|
||||
try {
|
||||
({ data: issue } = await github.rest.issues.get({
|
||||
owner, repo, issue_number: prNumber,
|
||||
}));
|
||||
} catch (e) {
|
||||
throw new Error(`Cannot fetch PR #${prNumber} issue data (HTTP ${e.status ?? 'unknown'}): ${e.message}`);
|
||||
}
|
||||
|
||||
const labels = (issue.labels || []).map(label => label.name);
|
||||
if (labels.includes('bypass-issue-check')) {
|
||||
console.log(`PR #${prNumber} already has bypass-issue-check — skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const body = issue.body || '';
|
||||
const referencedIssues = [...body.matchAll(pattern)].map(match => parseInt(match[1], 10));
|
||||
if (!referencedIssues.includes(issueNumber)) {
|
||||
console.log(`PR #${prNumber} does not reference #${issueNumber} — skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await mutate(`reopen PR #${prNumber}`, () => github.rest.pulls.update({
|
||||
owner, repo, pull_number: prNumber, state: 'open',
|
||||
}));
|
||||
} catch (e) {
|
||||
if (e.status === 422) {
|
||||
core.warning(`Cannot reopen PR #${prNumber}: the head branch was likely deleted`);
|
||||
await mutate(`comment on unreopenable PR #${prNumber}`, () => github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body:
|
||||
`You have been assigned to #${issueNumber}, but this PR could not be ` +
|
||||
'reopened because the head branch has been deleted. Please open a new PR ' +
|
||||
'referencing the issue.',
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
await mutate(`remove "${LABEL}" from PR #${prNumber}`, async () => {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner, repo, issue_number: prNumber, name: LABEL,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number: prNumber, per_page: 100 },
|
||||
);
|
||||
const stale = comments.find(comment => comment.body && comment.body.includes(MARKER));
|
||||
if (stale) {
|
||||
await mutate(`minimize stale comment ${stale.id}`, () => github.graphql(`
|
||||
mutation($id: ID!) {
|
||||
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
|
||||
minimizedComment { isMinimized }
|
||||
}
|
||||
}
|
||||
`, { id: stale.node_id }));
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Could not minimize stale comment on PR #${prNumber}: ${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner, repo, pull_number: prNumber,
|
||||
});
|
||||
const { data: runs } = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'require-issue-link.yml',
|
||||
head_sha: pr.head.sha,
|
||||
status: 'failure',
|
||||
per_page: 1,
|
||||
});
|
||||
if (runs.workflow_runs.length === 0) {
|
||||
console.log(`No failed require-issue-link runs found for PR #${prNumber}`);
|
||||
continue;
|
||||
}
|
||||
await mutate(`re-run failed require-issue-link run for PR #${prNumber}`, () =>
|
||||
github.rest.actions.reRunWorkflowFailedJobs({
|
||||
owner, repo, run_id: runs.workflow_runs[0].id,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
core.warning(`Could not re-run require-issue-link for PR #${prNumber}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Schema Crash Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "fastmcp_slim/fastmcp/utilities/json_schema_type.py"
|
||||
- "fastmcp_slim/fastmcp/utilities/json_schema.py"
|
||||
- "fastmcp_slim/fastmcp/utilities/openapi/**"
|
||||
- "fastmcp_slim/fastmcp/server/providers/openapi/**"
|
||||
- "fastmcp_slim/fastmcp/client/mixins/tools.py"
|
||||
- "tests/utilities/json_schema_type/test_real_world_schemas.py"
|
||||
- ".github/workflows/run-schema-crash-test.yml"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "fastmcp_slim/fastmcp/utilities/json_schema_type.py"
|
||||
- "fastmcp_slim/fastmcp/utilities/json_schema.py"
|
||||
- "fastmcp_slim/fastmcp/utilities/openapi/**"
|
||||
- "fastmcp_slim/fastmcp/server/providers/openapi/**"
|
||||
- "fastmcp_slim/fastmcp/client/mixins/tools.py"
|
||||
- "tests/utilities/json_schema_type/test_real_world_schemas.py"
|
||||
- ".github/workflows/run-schema-crash-test.yml"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
schema_crash_test:
|
||||
name: "Real-world schema crash test (232K schemas)"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.12
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Clone openapi-directory
|
||||
run: git clone --depth 1 https://github.com/APIs-guru/openapi-directory.git /tmp/openapi-directory
|
||||
|
||||
- name: Run schema crash test
|
||||
env:
|
||||
RUN_REAL_WORLD_SCHEMA_TEST: "1"
|
||||
OPENAPI_DIRECTORY_PATH: /tmp/openapi-directory
|
||||
run: uv run pytest tests/utilities/json_schema_type/test_real_world_schemas.py -m integration -v -n auto --timeout-method=thread
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Run static analysis
|
||||
|
||||
env:
|
||||
PY_COLORS: 1
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "fastmcp_slim/**"
|
||||
- "fastmcp_remote/**"
|
||||
- "tests/**"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
- ".github/workflows/**"
|
||||
|
||||
# run on all pull requests because these checks are required and will block merges otherwise
|
||||
pull_request:
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
static_analysis:
|
||||
timeout-minutes: 2
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup uv
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
resolution: locked
|
||||
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@v2
|
||||
env:
|
||||
SKIP: no-commit-to-branch
|
||||
@@ -0,0 +1,267 @@
|
||||
name: Tests
|
||||
|
||||
env:
|
||||
PY_COLORS: 1
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "fastmcp_slim/**"
|
||||
- "fastmcp_remote/**"
|
||||
- "tests/**"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
- ".github/workflows/**"
|
||||
|
||||
# run on all pull requests because these checks are required and will block merges otherwise
|
||||
pull_request:
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run_tests:
|
||||
name: "Tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python-version: ["3.10"]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.13"
|
||||
fail-fast: false
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup uv
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
resolution: locked
|
||||
|
||||
- name: Run unit tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
|
||||
- name: Run client process tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
with:
|
||||
test-type: client_process
|
||||
|
||||
run_tests_lowest_direct:
|
||||
name: "Tests with lowest-direct dependencies"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup uv (lowest-direct)
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
resolution: lowest-direct
|
||||
|
||||
- name: Run unit tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
|
||||
- name: Run client process tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
with:
|
||||
test-type: client_process
|
||||
|
||||
run_conformance_tests:
|
||||
name: "MCP conformance tests"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup uv
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
resolution: locked
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Run conformance tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
with:
|
||||
test-type: conformance
|
||||
|
||||
run_integration_tests:
|
||||
name: "Integration tests"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup uv
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
resolution: locked
|
||||
|
||||
- name: Run integration tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
with:
|
||||
test-type: integration
|
||||
env:
|
||||
FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }}
|
||||
FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID }}
|
||||
FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET }}
|
||||
|
||||
package_install_smoke:
|
||||
name: "Package install smoke"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup uv
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
resolution: locked
|
||||
|
||||
- name: Build package wheels
|
||||
run: uv build --all-packages --wheel --out-dir /tmp/fastmcp-dist
|
||||
|
||||
- name: Install bare slim wheel
|
||||
run: |
|
||||
uv venv /tmp/fastmcp-slim-bare-smoke
|
||||
SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
|
||||
uv pip install --python /tmp/fastmcp-slim-bare-smoke/bin/python "$SLIM_WHEEL"
|
||||
/tmp/fastmcp-slim-bare-smoke/bin/python - <<'PY'
|
||||
from importlib.metadata import entry_points
|
||||
|
||||
import fastmcp
|
||||
import fastmcp.settings
|
||||
|
||||
assert any(ep.name == "fastmcp" for ep in entry_points(group="console_scripts"))
|
||||
|
||||
try:
|
||||
from fastmcp.cli import app
|
||||
except ImportError as exc:
|
||||
assert "FastMCP CLI support is not installed" in str(exc)
|
||||
else:
|
||||
raise AssertionError(f"bare fastmcp-slim unexpectedly imported CLI app {app!r}")
|
||||
|
||||
try:
|
||||
fastmcp.FastMCP
|
||||
except ImportError as exc:
|
||||
assert "fastmcp-slim[server]" in str(exc)
|
||||
else:
|
||||
raise AssertionError("bare fastmcp-slim unexpectedly imported FastMCP")
|
||||
PY
|
||||
|
||||
- name: Install client slim wheel
|
||||
run: |
|
||||
uv venv /tmp/fastmcp-slim-client-smoke
|
||||
SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
|
||||
uv pip install --python /tmp/fastmcp-slim-client-smoke/bin/python "${SLIM_WHEEL}[client]"
|
||||
/tmp/fastmcp-slim-client-smoke/bin/python - <<'PY'
|
||||
from importlib.metadata import entry_points
|
||||
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.transports import StdioTransport, StreamableHttpTransport
|
||||
from fastmcp.mcp_config import MCPConfig
|
||||
|
||||
assert any(ep.name == "fastmcp" for ep in entry_points(group="console_scripts"))
|
||||
|
||||
try:
|
||||
from fastmcp.cli import app
|
||||
except ImportError as exc:
|
||||
assert "FastMCP CLI support is not installed" in str(exc)
|
||||
else:
|
||||
raise AssertionError(f"client-only slim unexpectedly imported CLI app {app!r}")
|
||||
|
||||
assert Client("https://example.com/mcp")
|
||||
assert StreamableHttpTransport("https://example.com/mcp")
|
||||
assert StdioTransport(command="uvx", args=["demo"])
|
||||
assert MCPConfig.from_dict({"mcpServers": {"demo": {"url": "https://example.com/mcp"}}})
|
||||
|
||||
try:
|
||||
from fastmcp import FastMCP
|
||||
except ImportError as exc:
|
||||
assert "fastmcp-slim[server]" in str(exc)
|
||||
else:
|
||||
raise AssertionError(f"client-only slim unexpectedly imported {FastMCP!r}")
|
||||
PY
|
||||
|
||||
- name: Install server slim wheel
|
||||
run: |
|
||||
uv venv /tmp/fastmcp-slim-server-smoke
|
||||
SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
|
||||
uv pip install --python /tmp/fastmcp-slim-server-smoke/bin/python "${SLIM_WHEEL}[server]"
|
||||
/tmp/fastmcp-slim-server-smoke/bin/python - <<'PY'
|
||||
from importlib.metadata import entry_points
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.cli import app
|
||||
|
||||
assert any(
|
||||
ep.name == "fastmcp" and ep.value == "fastmcp.cli:app"
|
||||
for ep in entry_points(group="console_scripts")
|
||||
)
|
||||
|
||||
mcp = FastMCP("smoke")
|
||||
assert app is not None
|
||||
assert mcp.name == "smoke"
|
||||
PY
|
||||
|
||||
- name: Install full package from matching local wheels
|
||||
run: |
|
||||
uv venv /tmp/fastmcp-full-smoke
|
||||
FULL_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp-*.whl)
|
||||
uv pip install --python /tmp/fastmcp-full-smoke/bin/python --prerelease=allow --find-links /tmp/fastmcp-dist "$FULL_WHEEL"
|
||||
/tmp/fastmcp-full-smoke/bin/python - <<'PY'
|
||||
from importlib.metadata import entry_points
|
||||
from importlib.metadata import requires
|
||||
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.client.client import CallToolResult
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
fastmcp_reqs = requires("fastmcp") or []
|
||||
assert any("fastmcp-slim[client,server]" in req for req in fastmcp_reqs)
|
||||
assert not any("fastmcp-slim[full" in req for req in fastmcp_reqs)
|
||||
|
||||
assert any(
|
||||
ep.name == "fastmcp" and ep.value == "fastmcp.cli:app"
|
||||
for ep in entry_points(group="console_scripts")
|
||||
)
|
||||
|
||||
assert Client("https://example.com/mcp")
|
||||
assert FastMCP("smoke").name == "smoke"
|
||||
assert CallToolResult is not None
|
||||
assert ToolError is not None
|
||||
PY
|
||||
|
||||
- name: Install fastmcp-remote from matching local wheels
|
||||
run: |
|
||||
uv venv /tmp/fastmcp-remote-smoke
|
||||
REMOTE_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_remote-*.whl)
|
||||
uv pip install --python /tmp/fastmcp-remote-smoke/bin/python --prerelease=allow --find-links /tmp/fastmcp-dist "$REMOTE_WHEEL"
|
||||
/tmp/fastmcp-remote-smoke/bin/python - <<'PY'
|
||||
from importlib.metadata import entry_points
|
||||
from importlib.metadata import requires
|
||||
|
||||
from fastmcp_remote.cli import build_parser
|
||||
|
||||
remote_reqs = requires("fastmcp-remote") or []
|
||||
assert any("fastmcp-slim[client,server]" in req for req in remote_reqs)
|
||||
assert any(
|
||||
ep.name == "fastmcp-remote" and ep.value == "fastmcp_remote.cli:main"
|
||||
for ep in entry_points(group="console_scripts")
|
||||
)
|
||||
assert build_parser().prog == "fastmcp-remote"
|
||||
PY
|
||||
@@ -0,0 +1,157 @@
|
||||
name: Upgrade checks
|
||||
|
||||
env:
|
||||
PY_COLORS: 1
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "fastmcp_slim/**"
|
||||
- "tests/**"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
- ".github/workflows/**"
|
||||
|
||||
schedule:
|
||||
# Run daily at 2 AM UTC
|
||||
- cron: "0 2 * * *"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
static_analysis:
|
||||
name: Static analysis
|
||||
timeout-minutes: 2
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup uv (upgrade)
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
resolution: upgrade
|
||||
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@v2
|
||||
env:
|
||||
SKIP: no-commit-to-branch
|
||||
|
||||
run_tests:
|
||||
name: "Tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python-version: ["3.10"]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.13"
|
||||
fail-fast: false
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup uv (upgrade)
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
resolution: upgrade
|
||||
|
||||
- name: Run unit tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
|
||||
- name: Run client process tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
with:
|
||||
test-type: client_process
|
||||
|
||||
run_integration_tests:
|
||||
name: "Integration tests"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup uv (upgrade)
|
||||
uses: ./.github/actions/setup-uv
|
||||
with:
|
||||
resolution: upgrade
|
||||
|
||||
- name: Run integration tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
with:
|
||||
test-type: integration
|
||||
env:
|
||||
FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }}
|
||||
FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID }}
|
||||
FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET }}
|
||||
|
||||
notify:
|
||||
name: Notify on failure
|
||||
needs: [static_analysis, run_tests, run_integration_tests]
|
||||
if: failure() && github.event.pull_request == null
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Create or update failure issue
|
||||
uses: jayqi/failed-build-issue-action@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
label: "build failed"
|
||||
title-template: "Upgrade checks failing on main branch"
|
||||
body-template: |
|
||||
## Upgrade Checks Failure on Main Branch
|
||||
|
||||
The upgrade checks workflow has failed on the main branch.
|
||||
|
||||
**Workflow Run**: [#{{runNumber}}]({{serverUrl}}/{{repo.owner}}/{{repo.repo}}/actions/runs/{{runId}})
|
||||
**Commit**: {{sha}}
|
||||
**Branch**: {{ref}}
|
||||
**Event**: {{eventName}}
|
||||
|
||||
### Common causes
|
||||
|
||||
- **ty (type checker)**: New ty releases frequently add stricter checks that flag previously-accepted code. Run `uv run ty check` locally with the latest ty to reproduce. Fix the type errors or bump the ty version floor in `pyproject.toml`.
|
||||
- **ruff**: New lint rules or stricter defaults in a ruff upgrade.
|
||||
- **MCP SDK**: Breaking changes in the `mcp` package (new method signatures, renamed types).
|
||||
|
||||
### What to do
|
||||
|
||||
1. Check the workflow logs to identify which job failed (static analysis vs tests)
|
||||
2. Reproduce locally with `uv sync --upgrade && uv run prek run --all-files && uv run pytest -n auto`
|
||||
3. Fix the code or adjust dependency constraints as needed
|
||||
|
||||
---
|
||||
*This issue was automatically created by a GitHub Action.*
|
||||
|
||||
close-on-success:
|
||||
name: Close issue on success
|
||||
needs: [static_analysis, run_tests, run_integration_tests]
|
||||
if: success() && github.event.pull_request == null && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Close resolved failure issue
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
issue=$(gh issue list \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--label "build failed" \
|
||||
--state open \
|
||||
--json number \
|
||||
--jq '.[0].number // empty')
|
||||
|
||||
if [ -n "$issue" ]; then
|
||||
gh issue close "$issue" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--comment "Upgrade checks are passing again as of [\`${GITHUB_SHA::7}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA})."
|
||||
fi
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Update MCPServerConfig Schema
|
||||
|
||||
# Regenerates config schema on pushes to main and opens a long-lived PR
|
||||
# with the changes, so contributor PRs stay clean.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "fastmcp_slim/fastmcp/utilities/mcp_server_config/**"
|
||||
- "!fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-config-schema:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
token: ${{ steps.marvin-token.outputs.token }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --python 3.12
|
||||
|
||||
- name: Generate config schema
|
||||
run: |
|
||||
uv run python -c "
|
||||
from fastmcp.utilities.mcp_server_config import generate_schema
|
||||
generate_schema('docs/public/schemas/fastmcp.json/latest.json')
|
||||
generate_schema('docs/public/schemas/fastmcp.json/v1.json')
|
||||
generate_schema('fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json')
|
||||
"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ steps.marvin-token.outputs.token }}
|
||||
commit-message: "chore: Update fastmcp.json schema"
|
||||
title: "chore: Update fastmcp.json schema"
|
||||
body: |
|
||||
This PR updates the fastmcp.json schema files to match the current source code.
|
||||
|
||||
The schema is automatically generated from `fastmcp_slim/fastmcp/utilities/mcp_server_config/` to ensure consistency.
|
||||
|
||||
**Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means.
|
||||
|
||||
🤖 Generated by Marvin
|
||||
branch: marvin/update-config-schema
|
||||
labels: |
|
||||
ignore in release notes
|
||||
delete-branch: true
|
||||
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
|
||||
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Update SDK Documentation
|
||||
|
||||
# Regenerates SDK docs on pushes to main and opens a long-lived PR
|
||||
# with the changes, so contributor PRs stay clean.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "fastmcp_slim/**"
|
||||
- "pyproject.toml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-sdk-docs:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
token: ${{ steps.marvin-token.outputs.token }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --python 3.12
|
||||
|
||||
- name: Install just
|
||||
uses: extractions/setup-just@v4
|
||||
|
||||
- name: Generate SDK documentation
|
||||
run: just api-ref-all
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ steps.marvin-token.outputs.token }}
|
||||
commit-message: "chore: Update SDK documentation"
|
||||
title: "chore: Update SDK documentation"
|
||||
body: |
|
||||
This PR updates the auto-generated SDK documentation to reflect the latest source code changes.
|
||||
|
||||
📚 Documentation is automatically generated from the source code docstrings and type annotations.
|
||||
|
||||
**Note:** This PR is fully automated and will update itself with any subsequent changes to the SDK, or close automatically if the documentation becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.
|
||||
|
||||
🤖 Generated by Marvin
|
||||
branch: marvin/update-sdk-docs
|
||||
labels: |
|
||||
ignore in release notes
|
||||
delete-branch: true
|
||||
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
|
||||
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
|
||||
Reference in New Issue
Block a user