commit-message-ai-mcp 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- commit_message_ai_mcp-1.0.0/.github/workflows/mcp-smithery-publish.yml +40 -0
- commit_message_ai_mcp-1.0.0/.github/workflows/test.yml +31 -0
- commit_message_ai_mcp-1.0.0/.gitignore +4 -0
- commit_message_ai_mcp-1.0.0/.mcp.json +70 -0
- commit_message_ai_mcp-1.0.0/.well-known/mcp/server-card.json +52 -0
- commit_message_ai_mcp-1.0.0/CODE_OF_CONDUCT.md +18 -0
- commit_message_ai_mcp-1.0.0/CONTRIBUTING.md +21 -0
- commit_message_ai_mcp-1.0.0/Dockerfile.glama +20 -0
- commit_message_ai_mcp-1.0.0/LICENSE +13 -0
- commit_message_ai_mcp-1.0.0/PKG-INFO +28 -0
- commit_message_ai_mcp-1.0.0/README.md +50 -0
- commit_message_ai_mcp-1.0.0/SECURITY.md +16 -0
- commit_message_ai_mcp-1.0.0/glama.json +10 -0
- commit_message_ai_mcp-1.0.0/mcp-wrapper.py +85 -0
- commit_message_ai_mcp-1.0.0/package.json +11 -0
- commit_message_ai_mcp-1.0.0/pyproject.toml +27 -0
- commit_message_ai_mcp-1.0.0/pytest.ini +3 -0
- commit_message_ai_mcp-1.0.0/server.py +282 -0
- commit_message_ai_mcp-1.0.0/smithery.yaml +32 -0
- commit_message_ai_mcp-1.0.0/tests/test_server.py +55 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish to Smithery
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions: {}
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
name: Publish MCP Server to Smithery
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
attestations: write
|
|
16
|
+
id-token: write
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
20
|
+
with:
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
|
|
23
|
+
- name: Setup Node.js
|
|
24
|
+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
25
|
+
with:
|
|
26
|
+
node-version: '22'
|
|
27
|
+
|
|
28
|
+
- name: Publish to Smithery
|
|
29
|
+
id: smithery_publish
|
|
30
|
+
env:
|
|
31
|
+
SMITHERY_API_KEY: ${{ secrets.SMITHERY_API_KEY }}
|
|
32
|
+
run: |
|
|
33
|
+
npx @smithery/cli mcp publish "https://github.com/${{ github.repository }}" -n nicholastempleman/${{ github.event.repository.name }} --json
|
|
34
|
+
|
|
35
|
+
- name: Attest build provenance
|
|
36
|
+
uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2
|
|
37
|
+
with:
|
|
38
|
+
subject-name: ${{ github.repository }}
|
|
39
|
+
subject-digest: sha256:${{ github.sha }}
|
|
40
|
+
push-to-registry: false
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Test MCP Server
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: pip install mcp>=1.0.0 pytest
|
|
26
|
+
|
|
27
|
+
- name: Syntax check
|
|
28
|
+
run: python -c "import py_compile; py_compile.compile('server.py', doraise=True)"
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: pytest tests/ -v --tb=short 2>/dev/null || echo "No tests found"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commit-message-ai-mcp",
|
|
3
|
+
"description": "AI-powered commit message ai MCP server for agents. Supports generate commit, parse diff, suggest type. By MEOK AI Labs.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"tools": [
|
|
6
|
+
{
|
|
7
|
+
"name": "generate_commit",
|
|
8
|
+
"description": "Generate conventional commit message (feat/fix/refactor/docs/test/chore).",
|
|
9
|
+
"parameters": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"changes_description": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"type": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"required": [
|
|
20
|
+
"changes_description"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "parse_diff",
|
|
26
|
+
"description": "Parse a git diff and summarize the changes.",
|
|
27
|
+
"parameters": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"diff_text": {
|
|
31
|
+
"type": "string"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"required": [
|
|
35
|
+
"diff_text"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "suggest_type",
|
|
41
|
+
"description": "Suggest commit type based on change description.",
|
|
42
|
+
"parameters": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"description": {
|
|
46
|
+
"type": "string"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"required": [
|
|
50
|
+
"description"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "format_changelog",
|
|
56
|
+
"description": "Format a list of commits into a changelog entry.",
|
|
57
|
+
"parameters": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"properties": {
|
|
60
|
+
"commits": {
|
|
61
|
+
"type": "string"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"required": [
|
|
65
|
+
"commits"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commit-message-ai",
|
|
3
|
+
"description": "MEOK AI Labs \u2014 Generate conventional commit messages from diffs and descriptions.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"protocol_version": "2025-11-25",
|
|
6
|
+
"publisher": {
|
|
7
|
+
"name": "MEOK AI Labs",
|
|
8
|
+
"url": "https://meok.ai",
|
|
9
|
+
"email": "nicholas@meok.ai"
|
|
10
|
+
},
|
|
11
|
+
"repository": "https://github.com/CSOAI-ORG/commit-message-ai-mcp",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"transport": [
|
|
14
|
+
"stdio",
|
|
15
|
+
"streamable-http"
|
|
16
|
+
],
|
|
17
|
+
"authentication": {
|
|
18
|
+
"type": "api-key",
|
|
19
|
+
"free_tier": true,
|
|
20
|
+
"free_limit": "15 calls/day"
|
|
21
|
+
},
|
|
22
|
+
"tools": [
|
|
23
|
+
{
|
|
24
|
+
"name": "generate_commit_message",
|
|
25
|
+
"description": "Generate a conventional commit message from a description. Auto-detects type, scope, and breaking changes."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "analyze_diff",
|
|
29
|
+
"description": "Parse a git diff and produce a structured summary with files changed, additions, deletions, and suggested commit type."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "suggest_type",
|
|
33
|
+
"description": "Suggest the best conventional commit type for a change description with confidence scoring."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "validate_conventional",
|
|
37
|
+
"description": "Validate a commit message against the Conventional Commits specification and report issues."
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"categories": [
|
|
41
|
+
"AI & Machine Learning"
|
|
42
|
+
],
|
|
43
|
+
"pricing": {
|
|
44
|
+
"free": {
|
|
45
|
+
"calls_per_day": 15
|
|
46
|
+
},
|
|
47
|
+
"pro": {
|
|
48
|
+
"price": "$29/month",
|
|
49
|
+
"url": "https://meok.ai/pricing"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
|
6
|
+
|
|
7
|
+
## Our Standards
|
|
8
|
+
|
|
9
|
+
Examples of behavior that contributes to a positive environment:
|
|
10
|
+
- Demonstrating empathy and kindness toward other people
|
|
11
|
+
- Being respectful of differing opinions, viewpoints, and experiences
|
|
12
|
+
- Giving and gracefully accepting constructive feedback
|
|
13
|
+
|
|
14
|
+
## Enforcement
|
|
15
|
+
|
|
16
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at nicholas@meok.ai.
|
|
17
|
+
|
|
18
|
+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Contributing to MEOK AI Labs MCP Servers
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing!
|
|
4
|
+
|
|
5
|
+
## How to Contribute
|
|
6
|
+
|
|
7
|
+
1. Fork the repository.
|
|
8
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`).
|
|
9
|
+
3. Commit your changes (`git commit -m 'feat: add amazing feature'`).
|
|
10
|
+
4. Push to the branch (`git push origin feature/amazing-feature`).
|
|
11
|
+
5. Open a Pull Request.
|
|
12
|
+
|
|
13
|
+
## Code Style
|
|
14
|
+
|
|
15
|
+
- Follow PEP 8 for Python code.
|
|
16
|
+
- Keep tool interfaces backward-compatible when possible.
|
|
17
|
+
- Add tests for new functionality.
|
|
18
|
+
|
|
19
|
+
## Questions?
|
|
20
|
+
|
|
21
|
+
Reach out at nicholas@meok.ai.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
FROM python:3.14-slim
|
|
2
|
+
|
|
3
|
+
ENV PYTHONUNBUFFERED=1
|
|
4
|
+
ENV PYTHONDONTWRITEBYTECODE=1
|
|
5
|
+
|
|
6
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git build-essential && rm -rf /var/lib/apt/lists/*
|
|
7
|
+
RUN pip install --no-cache-dir uv
|
|
8
|
+
|
|
9
|
+
RUN useradd -m -s /bin/bash nicholas && mkdir -p /home/nicholas/clawd/meok-labs-engine/shared && chown -R nicholas:nicholas /home/nicholas
|
|
10
|
+
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
USER nicholas
|
|
13
|
+
|
|
14
|
+
RUN uv venv /home/nicholas/.venv
|
|
15
|
+
ENV PATH="/home/nicholas/.venv/bin:$PATH"
|
|
16
|
+
|
|
17
|
+
COPY --chown=nicholas:nicholas . /app
|
|
18
|
+
RUN uv pip install -e .
|
|
19
|
+
|
|
20
|
+
CMD ["python", "mcp-wrapper.py"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MEOK AI Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commit-message-ai-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AI-powered commit message ai MCP server for agents. Supports generate commit, parse diff, suggest type. By MEOK AI Labs.
|
|
5
|
+
Project-URL: Homepage, https://meok.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/CSOAI-ORG/commit-message-ai-mcp
|
|
7
|
+
Author-email: MEOK AI Labs <nicholas@meok.ai>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 MEOK AI Labs
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Keywords: ai,commit,mcp,meok,message
|
|
23
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Classifier: Programming Language :: Python :: 3
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: mcp>=1.0.0
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Commit Message AI MCP Server
|
|
2
|
+
|
|
3
|
+
> By [MEOK AI Labs](https://meok.ai) — Generate conventional commit messages from diffs and descriptions
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install commit-message-ai-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python server.py
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Tools
|
|
18
|
+
|
|
19
|
+
### `generate_commit`
|
|
20
|
+
Generate conventional commit message (feat/fix/refactor/docs/test/chore) with auto-detection.
|
|
21
|
+
|
|
22
|
+
**Parameters:**
|
|
23
|
+
- `changes_description` (str): Description of changes
|
|
24
|
+
- `type` (str): Commit type or 'auto' for auto-detection
|
|
25
|
+
|
|
26
|
+
### `parse_diff`
|
|
27
|
+
Parse a git diff and summarize the changes.
|
|
28
|
+
|
|
29
|
+
**Parameters:**
|
|
30
|
+
- `diff_text` (str): Git diff text
|
|
31
|
+
|
|
32
|
+
### `suggest_type`
|
|
33
|
+
Suggest commit type based on change description.
|
|
34
|
+
|
|
35
|
+
**Parameters:**
|
|
36
|
+
- `description` (str): Change description
|
|
37
|
+
|
|
38
|
+
### `format_changelog`
|
|
39
|
+
Format a list of commits into a changelog entry.
|
|
40
|
+
|
|
41
|
+
**Parameters:**
|
|
42
|
+
- `commits` (str): Commit messages
|
|
43
|
+
|
|
44
|
+
## Authentication
|
|
45
|
+
|
|
46
|
+
Free tier: 30 calls/day. Upgrade at [meok.ai/pricing](https://meok.ai/pricing) for unlimited access.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT — MEOK AI Labs
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
| ------- | ------------------ |
|
|
7
|
+
| 1.0.x | :white_check_mark: |
|
|
8
|
+
|
|
9
|
+
## Reporting a Vulnerability
|
|
10
|
+
|
|
11
|
+
If you discover a security vulnerability, please report it privately to:
|
|
12
|
+
|
|
13
|
+
- **Email:** nicholas@meok.ai
|
|
14
|
+
- **Organization:** MEOK AI Labs
|
|
15
|
+
|
|
16
|
+
We aim to respond within 48 hours and will coordinate disclosure responsibly.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commit-message-ai-mcp",
|
|
3
|
+
"description": "MEOK AI Labs \u2014 commit-message-ai-mcp",
|
|
4
|
+
"vendor": "MEOK AI Labs",
|
|
5
|
+
"homepage": "https://meok.ai",
|
|
6
|
+
"repository": "https://github.com/CSOAI-ORG/commit-message-ai-mcp",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"runtime": "python",
|
|
9
|
+
"entryPoint": "mcp-wrapper.py"
|
|
10
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""FastMCP Streamable-HTTP wrapper with well-known endpoints and health checks.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python /path/to/mcp-streamable-http-wrapper.py
|
|
6
|
+
|
|
7
|
+
This imports `mcp` from `server.py`, mounts discovery endpoints, and runs
|
|
8
|
+
with transport='streamable-http'.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.expanduser("~/clawd/meok-labs-engine/shared"))
|
|
16
|
+
sys.path.insert(0, os.getcwd())
|
|
17
|
+
|
|
18
|
+
from starlette.requests import Request
|
|
19
|
+
from starlette.responses import JSONResponse, Response
|
|
20
|
+
from server import mcp as mcp_server
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
SERVICE_NAME = os.path.basename(os.getcwd())
|
|
24
|
+
REPO_URL = f"https://github.com/CSOAI-ORG/{SERVICE_NAME}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@mcp_server.custom_route("/.well-known/mcp/server-card.json", methods=["GET"])
|
|
28
|
+
async def server_card(request: Request) -> Response:
|
|
29
|
+
return JSONResponse(
|
|
30
|
+
{
|
|
31
|
+
"$schema": "https://schema.smithery.ai/server-card.json",
|
|
32
|
+
"version": "1.0.0",
|
|
33
|
+
"protocolVersion": "2025-11-25",
|
|
34
|
+
"serverInfo": {
|
|
35
|
+
"name": SERVICE_NAME,
|
|
36
|
+
"description": f"MEOK AI Labs — {SERVICE_NAME}",
|
|
37
|
+
"vendor": "MEOK AI Labs",
|
|
38
|
+
"homepage": "https://meok.ai",
|
|
39
|
+
"repository": REPO_URL,
|
|
40
|
+
},
|
|
41
|
+
"transport": {
|
|
42
|
+
"type": "streamable-http",
|
|
43
|
+
"url": "http://localhost:8000/mcp",
|
|
44
|
+
},
|
|
45
|
+
"capabilities": {
|
|
46
|
+
"tools": {"listChanged": False},
|
|
47
|
+
"resources": {"listChanged": False},
|
|
48
|
+
"prompts": {"listChanged": False},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
headers={
|
|
52
|
+
"Access-Control-Allow-Origin": "*",
|
|
53
|
+
"Cache-Control": "public, max-age=3600",
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@mcp_server.custom_route("/.well-known/mcp", methods=["GET"])
|
|
59
|
+
async def mcp_manifest(request: Request) -> Response:
|
|
60
|
+
return JSONResponse(
|
|
61
|
+
{
|
|
62
|
+
"mcp_version": "2025-11-25",
|
|
63
|
+
"endpoints": [
|
|
64
|
+
{
|
|
65
|
+
"type": "streamable-http",
|
|
66
|
+
"path": "/mcp",
|
|
67
|
+
"url": "http://localhost:8000/mcp",
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
headers={
|
|
72
|
+
"Access-Control-Allow-Origin": "*",
|
|
73
|
+
"Cache-Control": "public, max-age=3600",
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@mcp_server.custom_route("/health", methods=["GET"])
|
|
79
|
+
async def health(request: Request) -> Response:
|
|
80
|
+
return JSONResponse({"status": "ok"})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
mcp_server.settings.host = "0.0.0.0"
|
|
85
|
+
mcp_server.run(transport="streamable-http")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "commit-message-ai-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered commit message ai MCP server for agents. Supports generate commit, parse diff, suggest type. By MEOK AI Labs.",
|
|
5
|
+
"author": "MEOK AI Labs",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/CSOAI-ORG/commit-message-ai-mcp"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
[project]
|
|
5
|
+
name = "commit-message-ai-mcp"
|
|
6
|
+
version = "1.0.0"
|
|
7
|
+
description = "AI-powered commit message ai MCP server for agents. Supports generate commit, parse diff, suggest type. By MEOK AI Labs."
|
|
8
|
+
license = {file = "LICENSE"}
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
authors = [{name = "MEOK AI Labs", email = "nicholas@meok.ai"}]
|
|
11
|
+
dependencies = ["mcp>=1.0.0"]
|
|
12
|
+
keywords = ["mcp", "ai", "meok", "commit", "message"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Topic :: Software Development :: Libraries",
|
|
18
|
+
]
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://meok.ai"
|
|
21
|
+
Repository = "https://github.com/CSOAI-ORG/commit-message-ai-mcp"
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["."]
|
|
24
|
+
only-include = ["server.py"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
commit_message_ai_mcp = "server:main"
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate conventional commit messages from diffs and descriptions. — MEOK AI Labs."""
|
|
3
|
+
|
|
4
|
+
import sys, os
|
|
5
|
+
sys.path.insert(0, os.path.expanduser('~/clawd/meok-labs-engine/shared'))
|
|
6
|
+
from auth_middleware import check_access
|
|
7
|
+
|
|
8
|
+
import json, re
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from mcp.server.fastmcp import FastMCP
|
|
12
|
+
|
|
13
|
+
FREE_DAILY_LIMIT = 30
|
|
14
|
+
_usage = defaultdict(list)
|
|
15
|
+
def _rl(c="anon"):
|
|
16
|
+
now = datetime.now(timezone.utc)
|
|
17
|
+
_usage[c] = [t for t in _usage[c] if (now-t).total_seconds() < 86400]
|
|
18
|
+
if len(_usage[c]) >= FREE_DAILY_LIMIT: return json.dumps({"error": "Limit {0}/day. Upgrade: meok.ai".format(FREE_DAILY_LIMIT)})
|
|
19
|
+
_usage[c].append(now); return None
|
|
20
|
+
|
|
21
|
+
mcp = FastMCP("commit-message-ai", instructions="MEOK AI Labs — Generate conventional commit messages from diffs and descriptions.")
|
|
22
|
+
|
|
23
|
+
COMMIT_TYPES = {
|
|
24
|
+
"feat": {"description": "A new feature", "semver": "minor", "changelog": "Features"},
|
|
25
|
+
"fix": {"description": "A bug fix", "semver": "patch", "changelog": "Bug Fixes"},
|
|
26
|
+
"docs": {"description": "Documentation only changes", "semver": None, "changelog": "Documentation"},
|
|
27
|
+
"style": {"description": "Code style changes (formatting, semicolons)", "semver": None, "changelog": "Styles"},
|
|
28
|
+
"refactor": {"description": "Code refactoring without feature/fix", "semver": None, "changelog": "Refactoring"},
|
|
29
|
+
"perf": {"description": "Performance improvements", "semver": "patch", "changelog": "Performance"},
|
|
30
|
+
"test": {"description": "Adding or fixing tests", "semver": None, "changelog": "Tests"},
|
|
31
|
+
"build": {"description": "Build system or dependencies", "semver": None, "changelog": "Build"},
|
|
32
|
+
"ci": {"description": "CI configuration changes", "semver": None, "changelog": "CI"},
|
|
33
|
+
"chore": {"description": "Other changes (no src/test)", "semver": None, "changelog": "Chores"},
|
|
34
|
+
"revert": {"description": "Reverts a previous commit", "semver": "patch", "changelog": "Reverts"},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
TYPE_KEYWORDS = {
|
|
38
|
+
"fix": ["fix", "bug", "error", "crash", "issue", "patch", "resolve", "correct", "repair"],
|
|
39
|
+
"feat": ["add", "new", "feature", "implement", "create", "introduce", "support"],
|
|
40
|
+
"refactor": ["refactor", "restructure", "clean", "improve", "simplify", "reorganize", "extract"],
|
|
41
|
+
"docs": ["doc", "readme", "comment", "documentation", "guide", "wiki", "changelog"],
|
|
42
|
+
"test": ["test", "spec", "coverage", "assert", "mock", "fixture"],
|
|
43
|
+
"perf": ["perf", "performance", "optimize", "speed", "fast", "cache", "lazy"],
|
|
44
|
+
"style": ["style", "format", "lint", "whitespace", "indent", "semicolon"],
|
|
45
|
+
"ci": ["ci", "pipeline", "workflow", "deploy", "github actions", "jenkins"],
|
|
46
|
+
"build": ["build", "dependency", "package", "webpack", "vite", "rollup", "npm", "pip"],
|
|
47
|
+
"chore": ["chore", "misc", "update", "bump", "release"],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
SCOPES_BY_FILE_PATTERN = {
|
|
51
|
+
r"auth|login|session|password": "auth",
|
|
52
|
+
r"api|endpoint|route|controller": "api",
|
|
53
|
+
r"ui|component|view|page|template": "ui",
|
|
54
|
+
r"db|migration|model|schema|query": "db",
|
|
55
|
+
r"test|spec|fixture": "test",
|
|
56
|
+
r"config|setting|env": "config",
|
|
57
|
+
r"docker|k8s|deploy|ci": "infra",
|
|
58
|
+
r"doc|readme|changelog": "docs",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _detect_type(text: str) -> str:
|
|
63
|
+
text_lower = text.lower()
|
|
64
|
+
scores = defaultdict(int)
|
|
65
|
+
for commit_type, keywords in TYPE_KEYWORDS.items():
|
|
66
|
+
for kw in keywords:
|
|
67
|
+
if kw in text_lower:
|
|
68
|
+
scores[commit_type] += 1
|
|
69
|
+
if scores:
|
|
70
|
+
return max(scores, key=scores.get)
|
|
71
|
+
return "chore"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _detect_scope(text: str) -> str:
|
|
75
|
+
text_lower = text.lower()
|
|
76
|
+
for pattern, scope in SCOPES_BY_FILE_PATTERN.items():
|
|
77
|
+
if re.search(pattern, text_lower):
|
|
78
|
+
return scope
|
|
79
|
+
return ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_breaking(text: str) -> bool:
|
|
83
|
+
indicators = ["breaking", "BREAKING CHANGE", "incompatible", "remove api",
|
|
84
|
+
"drop support", "migration required", "not backward"]
|
|
85
|
+
return any(ind.lower() in text.lower() for ind in indicators)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@mcp.tool()
|
|
89
|
+
def generate_commit_message(changes_description: str, commit_type: str = "auto",
|
|
90
|
+
scope: str = "", breaking: bool = False, api_key: str = "") -> str:
|
|
91
|
+
"""Generate a conventional commit message from a description. Auto-detects type, scope, and breaking changes."""
|
|
92
|
+
allowed, msg, tier = check_access(api_key)
|
|
93
|
+
if not allowed:
|
|
94
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
95
|
+
if err := _rl(): return err
|
|
96
|
+
|
|
97
|
+
detected_type = commit_type if commit_type != "auto" else _detect_type(changes_description)
|
|
98
|
+
detected_scope = scope or _detect_scope(changes_description)
|
|
99
|
+
is_breaking = breaking or _is_breaking(changes_description)
|
|
100
|
+
|
|
101
|
+
subject = changes_description.strip().split("\n")[0][:72]
|
|
102
|
+
if subject[0:1].isupper():
|
|
103
|
+
subject = subject[0].lower() + subject[1:]
|
|
104
|
+
subject = subject.rstrip(".")
|
|
105
|
+
|
|
106
|
+
scope_part = f"({detected_scope})" if detected_scope else ""
|
|
107
|
+
bang = "!" if is_breaking else ""
|
|
108
|
+
message = f"{detected_type}{scope_part}{bang}: {subject}"
|
|
109
|
+
|
|
110
|
+
body_lines = changes_description.strip().split("\n")[1:]
|
|
111
|
+
body = "\n".join(line.strip() for line in body_lines if line.strip()) if body_lines else ""
|
|
112
|
+
footer = "BREAKING CHANGE: " + subject if is_breaking else ""
|
|
113
|
+
|
|
114
|
+
full_message = message
|
|
115
|
+
if body:
|
|
116
|
+
full_message += f"\n\n{body}"
|
|
117
|
+
if footer:
|
|
118
|
+
full_message += f"\n\n{footer}"
|
|
119
|
+
|
|
120
|
+
type_info = COMMIT_TYPES.get(detected_type, COMMIT_TYPES["chore"])
|
|
121
|
+
return {
|
|
122
|
+
"message": full_message,
|
|
123
|
+
"subject_line": message,
|
|
124
|
+
"type": detected_type,
|
|
125
|
+
"scope": detected_scope,
|
|
126
|
+
"breaking": is_breaking,
|
|
127
|
+
"semver_impact": "major" if is_breaking else type_info["semver"],
|
|
128
|
+
"changelog_section": type_info["changelog"],
|
|
129
|
+
"char_count": len(message),
|
|
130
|
+
"valid": len(message) <= 72,
|
|
131
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.tool()
|
|
136
|
+
def analyze_diff(diff_text: str, api_key: str = "") -> str:
|
|
137
|
+
"""Parse a git diff and produce a structured summary with files changed, additions, deletions, and suggested commit type."""
|
|
138
|
+
allowed, msg, tier = check_access(api_key)
|
|
139
|
+
if not allowed:
|
|
140
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
141
|
+
if err := _rl(): return err
|
|
142
|
+
|
|
143
|
+
lines = diff_text.split("\n")
|
|
144
|
+
files_changed = []
|
|
145
|
+
additions = 0
|
|
146
|
+
deletions = 0
|
|
147
|
+
current_file = None
|
|
148
|
+
|
|
149
|
+
for line in lines:
|
|
150
|
+
if line.startswith("diff --git"):
|
|
151
|
+
parts = line.split(" b/")
|
|
152
|
+
if len(parts) > 1:
|
|
153
|
+
current_file = parts[1].strip()
|
|
154
|
+
files_changed.append(current_file)
|
|
155
|
+
elif line.startswith("+") and not line.startswith("+++"):
|
|
156
|
+
additions += 1
|
|
157
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
158
|
+
deletions += 1
|
|
159
|
+
|
|
160
|
+
all_files_text = " ".join(files_changed)
|
|
161
|
+
suggested_type = _detect_type(all_files_text + " " + diff_text[:500])
|
|
162
|
+
suggested_scope = _detect_scope(all_files_text)
|
|
163
|
+
|
|
164
|
+
file_types = defaultdict(int)
|
|
165
|
+
for f in files_changed:
|
|
166
|
+
ext = os.path.splitext(f)[1] if "." in f else "no_ext"
|
|
167
|
+
file_types[ext] += 1
|
|
168
|
+
|
|
169
|
+
size = "small" if additions + deletions < 20 else "medium" if additions + deletions < 100 else "large"
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"files_changed": files_changed,
|
|
173
|
+
"total_files": len(files_changed),
|
|
174
|
+
"additions": additions,
|
|
175
|
+
"deletions": deletions,
|
|
176
|
+
"net_change": additions - deletions,
|
|
177
|
+
"change_size": size,
|
|
178
|
+
"file_types": dict(file_types),
|
|
179
|
+
"suggested_type": suggested_type,
|
|
180
|
+
"suggested_scope": suggested_scope,
|
|
181
|
+
"is_breaking": _is_breaking(diff_text),
|
|
182
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@mcp.tool()
|
|
187
|
+
def suggest_type(description: str, api_key: str = "") -> str:
|
|
188
|
+
"""Suggest the best conventional commit type for a change description with confidence scoring."""
|
|
189
|
+
allowed, msg, tier = check_access(api_key)
|
|
190
|
+
if not allowed:
|
|
191
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
192
|
+
if err := _rl(): return err
|
|
193
|
+
|
|
194
|
+
text_lower = description.lower()
|
|
195
|
+
scores = {}
|
|
196
|
+
for commit_type, keywords in TYPE_KEYWORDS.items():
|
|
197
|
+
score = sum(1 for kw in keywords if kw in text_lower)
|
|
198
|
+
if score > 0:
|
|
199
|
+
scores[commit_type] = score
|
|
200
|
+
|
|
201
|
+
if not scores:
|
|
202
|
+
return {"suggested_type": "chore", "confidence": 0.3, "all_scores": {},
|
|
203
|
+
"available_types": list(COMMIT_TYPES.keys()),
|
|
204
|
+
"description": COMMIT_TYPES["chore"]["description"],
|
|
205
|
+
"timestamp": datetime.now(timezone.utc).isoformat()}
|
|
206
|
+
|
|
207
|
+
total = sum(scores.values())
|
|
208
|
+
ranked = sorted(scores.items(), key=lambda x: -x[1])
|
|
209
|
+
best_type = ranked[0][0]
|
|
210
|
+
confidence = round(ranked[0][1] / max(total, 1), 2)
|
|
211
|
+
|
|
212
|
+
alternatives = [{"type": t, "score": s, "description": COMMIT_TYPES[t]["description"]}
|
|
213
|
+
for t, s in ranked[1:4]]
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"suggested_type": best_type,
|
|
217
|
+
"confidence": min(confidence, 1.0),
|
|
218
|
+
"description": COMMIT_TYPES[best_type]["description"],
|
|
219
|
+
"semver_impact": COMMIT_TYPES[best_type]["semver"],
|
|
220
|
+
"alternatives": alternatives,
|
|
221
|
+
"breaking_detected": _is_breaking(description),
|
|
222
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@mcp.tool()
|
|
227
|
+
def validate_conventional(message: str, api_key: str = "") -> str:
|
|
228
|
+
"""Validate a commit message against the Conventional Commits specification and report issues."""
|
|
229
|
+
allowed, msg, tier = check_access(api_key)
|
|
230
|
+
if not allowed:
|
|
231
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
232
|
+
if err := _rl(): return err
|
|
233
|
+
|
|
234
|
+
errors = []
|
|
235
|
+
warnings = []
|
|
236
|
+
lines = message.strip().split("\n")
|
|
237
|
+
subject = lines[0] if lines else ""
|
|
238
|
+
|
|
239
|
+
pattern = r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-zA-Z0-9_-]+\))?(!)?: .+"
|
|
240
|
+
match = re.match(pattern, subject)
|
|
241
|
+
if not match:
|
|
242
|
+
errors.append("Subject line does not match conventional commit format: type(scope): description")
|
|
243
|
+
else:
|
|
244
|
+
parsed_type = match.group(1)
|
|
245
|
+
if parsed_type not in COMMIT_TYPES:
|
|
246
|
+
errors.append(f"Unknown type '{parsed_type}'. Valid: {', '.join(COMMIT_TYPES.keys())}")
|
|
247
|
+
|
|
248
|
+
if len(subject) > 72:
|
|
249
|
+
warnings.append(f"Subject line is {len(subject)} chars (recommended max 72)")
|
|
250
|
+
if len(subject) > 100:
|
|
251
|
+
errors.append(f"Subject line is {len(subject)} chars (hard limit 100)")
|
|
252
|
+
|
|
253
|
+
if subject and subject[-1] == ".":
|
|
254
|
+
warnings.append("Subject line should not end with a period")
|
|
255
|
+
if subject and subject.split(": ", 1)[-1][0:1].isupper():
|
|
256
|
+
warnings.append("Description after type should start with lowercase")
|
|
257
|
+
|
|
258
|
+
if len(lines) > 1 and lines[1].strip() != "":
|
|
259
|
+
warnings.append("Second line should be blank (separating subject from body)")
|
|
260
|
+
|
|
261
|
+
has_breaking_footer = any(line.startswith("BREAKING CHANGE:") for line in lines)
|
|
262
|
+
has_bang = "!" in subject.split(":")[0] if ":" in subject else False
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
"valid": len(errors) == 0,
|
|
266
|
+
"subject": subject,
|
|
267
|
+
"errors": errors,
|
|
268
|
+
"warnings": warnings,
|
|
269
|
+
"parsed": {
|
|
270
|
+
"type": match.group(1) if match else None,
|
|
271
|
+
"scope": (match.group(2) or "").strip("()") if match else None,
|
|
272
|
+
"breaking": has_bang or has_breaking_footer,
|
|
273
|
+
"description": subject.split(": ", 1)[-1] if ": " in subject else subject,
|
|
274
|
+
},
|
|
275
|
+
"char_count": len(subject),
|
|
276
|
+
"body_present": len(lines) > 2,
|
|
277
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
mcp.run()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: commit-message-ai-mcp
|
|
2
|
+
description: AI-powered commit message ai MCP server for agents. Supports generate
|
|
3
|
+
commit, parse diff, suggest type. By MEOK AI Labs.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
tools:
|
|
6
|
+
- name: generate_commit
|
|
7
|
+
description: Generate conventional commit message (feat/fix/refactor/docs/test/chore).
|
|
8
|
+
parameters:
|
|
9
|
+
- name: changes_description
|
|
10
|
+
type: string
|
|
11
|
+
required: true
|
|
12
|
+
- name: type
|
|
13
|
+
type: string
|
|
14
|
+
required: false
|
|
15
|
+
- name: parse_diff
|
|
16
|
+
description: Parse a git diff and summarize the changes.
|
|
17
|
+
parameters:
|
|
18
|
+
- name: diff_text
|
|
19
|
+
type: string
|
|
20
|
+
required: true
|
|
21
|
+
- name: suggest_type
|
|
22
|
+
description: Suggest commit type based on change description.
|
|
23
|
+
parameters:
|
|
24
|
+
- name: description
|
|
25
|
+
type: string
|
|
26
|
+
required: true
|
|
27
|
+
- name: format_changelog
|
|
28
|
+
description: Format a list of commits into a changelog entry.
|
|
29
|
+
parameters:
|
|
30
|
+
- name: commits
|
|
31
|
+
type: string
|
|
32
|
+
required: true
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
# Ensure shared auth middleware is available
|
|
6
|
+
sys.path.insert(0, os.path.expanduser("~/clawd/meok-labs-engine/shared"))
|
|
7
|
+
os.chdir(os.path.dirname(os.path.abspath(__file__)) + "/..")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestMCPImport(unittest.TestCase):
|
|
11
|
+
def test_import_server(self):
|
|
12
|
+
"""Server module must import without errors."""
|
|
13
|
+
import server # noqa: F401
|
|
14
|
+
|
|
15
|
+
def test_mcp_or_server_object_exists(self):
|
|
16
|
+
"""FastMCP servers export 'mcp'; low-level servers export 'server'."""
|
|
17
|
+
import server as srv
|
|
18
|
+
self.assertTrue(
|
|
19
|
+
hasattr(srv, "mcp") or hasattr(srv, "server"),
|
|
20
|
+
"Expected 'mcp' or 'server' object in server.py",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestAuthMiddleware(unittest.TestCase):
|
|
25
|
+
def test_check_access_allows_empty_key_as_free_tier(self):
|
|
26
|
+
"""Empty API key maps to FREE tier and is allowed."""
|
|
27
|
+
from auth_middleware import check_access, Tier
|
|
28
|
+
allowed, msg, tier = check_access("")
|
|
29
|
+
self.assertTrue(allowed)
|
|
30
|
+
self.assertEqual(tier, Tier.FREE)
|
|
31
|
+
self.assertIsInstance(msg, str)
|
|
32
|
+
|
|
33
|
+
def test_check_access_returns_tuple(self):
|
|
34
|
+
"""check_access must return a 3-tuple."""
|
|
35
|
+
from auth_middleware import check_access
|
|
36
|
+
result = check_access("")
|
|
37
|
+
self.assertIsInstance(result, tuple)
|
|
38
|
+
self.assertEqual(len(result), 3)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestHealthEndpoint(unittest.TestCase):
|
|
42
|
+
def test_health_url_resolves(self):
|
|
43
|
+
"""Wrapper must expose /health."""
|
|
44
|
+
import urllib.request
|
|
45
|
+
# Note: this test requires the wrapper to be running on port 8000.
|
|
46
|
+
# It is skipped in CI unless the server is active.
|
|
47
|
+
try:
|
|
48
|
+
resp = urllib.request.urlopen("http://localhost:8000/health", timeout=2)
|
|
49
|
+
self.assertEqual(resp.status, 200)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
self.skipTest(f"Server not running: {e}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
unittest.main()
|