mcp-sonarcloud 0.1.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.
- mcp_sonarcloud-0.1.0/.github/workflows/publish.yml +85 -0
- mcp_sonarcloud-0.1.0/.github/workflows/test.yml +108 -0
- mcp_sonarcloud-0.1.0/.gitignore +47 -0
- mcp_sonarcloud-0.1.0/DEVELOPMENT.md +184 -0
- mcp_sonarcloud-0.1.0/LICENSE +21 -0
- mcp_sonarcloud-0.1.0/PKG-INFO +365 -0
- mcp_sonarcloud-0.1.0/README.md +337 -0
- mcp_sonarcloud-0.1.0/RELEASING.md +44 -0
- mcp_sonarcloud-0.1.0/SONARCLOUD_API_SUPPORT.md +66 -0
- mcp_sonarcloud-0.1.0/config.toml.example +8 -0
- mcp_sonarcloud-0.1.0/pyproject.toml +42 -0
- mcp_sonarcloud-0.1.0/src/mcp_sonarcloud/__init__.py +3 -0
- mcp_sonarcloud-0.1.0/src/mcp_sonarcloud/runtime_paths.py +73 -0
- mcp_sonarcloud-0.1.0/src/mcp_sonarcloud/server.py +963 -0
- mcp_sonarcloud-0.1.0/tests/__init__.py +1 -0
- mcp_sonarcloud-0.1.0/tests/test_server.py +954 -0
- mcp_sonarcloud-0.1.0/uv.lock +784 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
environment:
|
|
15
|
+
name: pypi
|
|
16
|
+
url: https://pypi.org/p/mcp-sonarcloud
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout code
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python 3.12
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.12"
|
|
26
|
+
|
|
27
|
+
- name: Install uv
|
|
28
|
+
uses: astral-sh/setup-uv@v3
|
|
29
|
+
with:
|
|
30
|
+
version: "latest"
|
|
31
|
+
|
|
32
|
+
- name: Validate tag matches package version
|
|
33
|
+
run: |
|
|
34
|
+
package_version="$(python -c 'import tomllib; from pathlib import Path; print(tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]["version"])')"
|
|
35
|
+
tag_version="${GITHUB_REF_NAME#v}"
|
|
36
|
+
|
|
37
|
+
if [ "$package_version" != "$tag_version" ]; then
|
|
38
|
+
echo "Tag version $tag_version does not match pyproject.toml version $package_version" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
- name: Install dependencies
|
|
43
|
+
run: |
|
|
44
|
+
uv sync --extra dev
|
|
45
|
+
|
|
46
|
+
- name: Run tests
|
|
47
|
+
run: |
|
|
48
|
+
uv run pytest -q
|
|
49
|
+
|
|
50
|
+
- name: Build package artifacts
|
|
51
|
+
run: |
|
|
52
|
+
uv build
|
|
53
|
+
|
|
54
|
+
- name: Smoke test wheel
|
|
55
|
+
run: |
|
|
56
|
+
tmp_dir="$(mktemp -d)"
|
|
57
|
+
uvx --from dist/*.whl mcp-sonarcloud \
|
|
58
|
+
--config-dir "$tmp_dir/config" \
|
|
59
|
+
--state-dir "$tmp_dir/state" \
|
|
60
|
+
--cache-dir "$tmp_dir/cache" \
|
|
61
|
+
--write-sample-config
|
|
62
|
+
test -f "$tmp_dir/config/config.toml"
|
|
63
|
+
uvx --from dist/*.whl mcp-sonarcloud \
|
|
64
|
+
--config-dir "$tmp_dir/config" \
|
|
65
|
+
--state-dir "$tmp_dir/state" \
|
|
66
|
+
--cache-dir "$tmp_dir/cache" \
|
|
67
|
+
--print-paths
|
|
68
|
+
|
|
69
|
+
- name: Smoke test sdist
|
|
70
|
+
run: |
|
|
71
|
+
tmp_dir="$(mktemp -d)"
|
|
72
|
+
uvx --from dist/*.tar.gz mcp-sonarcloud \
|
|
73
|
+
--config-dir "$tmp_dir/config" \
|
|
74
|
+
--state-dir "$tmp_dir/state" \
|
|
75
|
+
--cache-dir "$tmp_dir/cache" \
|
|
76
|
+
--write-sample-config
|
|
77
|
+
test -f "$tmp_dir/config/config.toml"
|
|
78
|
+
uvx --from dist/*.tar.gz mcp-sonarcloud \
|
|
79
|
+
--config-dir "$tmp_dir/config" \
|
|
80
|
+
--state-dir "$tmp_dir/state" \
|
|
81
|
+
--cache-dir "$tmp_dir/cache" \
|
|
82
|
+
--print-paths
|
|
83
|
+
|
|
84
|
+
- name: Publish to PyPI
|
|
85
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install uv
|
|
27
|
+
uses: astral-sh/setup-uv@v3
|
|
28
|
+
with:
|
|
29
|
+
version: "latest"
|
|
30
|
+
|
|
31
|
+
- name: Cache uv environment
|
|
32
|
+
uses: actions/cache@v4
|
|
33
|
+
with:
|
|
34
|
+
path: |
|
|
35
|
+
~/.cache/uv
|
|
36
|
+
.venv
|
|
37
|
+
key: ${{ runner.os }}-uv-py${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
|
|
38
|
+
restore-keys: |
|
|
39
|
+
${{ runner.os }}-uv-py${{ matrix.python-version }}-
|
|
40
|
+
|
|
41
|
+
- name: Install dependencies
|
|
42
|
+
run: |
|
|
43
|
+
uv sync --extra dev
|
|
44
|
+
|
|
45
|
+
- name: Run tests
|
|
46
|
+
run: |
|
|
47
|
+
uv run pytest --junitxml=test-results/junit.xml
|
|
48
|
+
|
|
49
|
+
- name: Upload test results
|
|
50
|
+
if: always()
|
|
51
|
+
uses: actions/upload-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: test-results-py${{ matrix.python-version }}
|
|
54
|
+
path: |
|
|
55
|
+
test-results/
|
|
56
|
+
if-no-files-found: ignore
|
|
57
|
+
|
|
58
|
+
package-smoke:
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
needs: test
|
|
61
|
+
|
|
62
|
+
steps:
|
|
63
|
+
- name: Checkout code
|
|
64
|
+
uses: actions/checkout@v4
|
|
65
|
+
|
|
66
|
+
- name: Set up Python 3.12
|
|
67
|
+
uses: actions/setup-python@v5
|
|
68
|
+
with:
|
|
69
|
+
python-version: "3.12"
|
|
70
|
+
|
|
71
|
+
- name: Install uv
|
|
72
|
+
uses: astral-sh/setup-uv@v3
|
|
73
|
+
with:
|
|
74
|
+
version: "latest"
|
|
75
|
+
|
|
76
|
+
- name: Build package artifacts
|
|
77
|
+
run: |
|
|
78
|
+
uv build
|
|
79
|
+
|
|
80
|
+
- name: Smoke test wheel
|
|
81
|
+
run: |
|
|
82
|
+
tmp_dir="$(mktemp -d)"
|
|
83
|
+
uvx --from dist/*.whl mcp-sonarcloud \
|
|
84
|
+
--config-dir "$tmp_dir/config" \
|
|
85
|
+
--state-dir "$tmp_dir/state" \
|
|
86
|
+
--cache-dir "$tmp_dir/cache" \
|
|
87
|
+
--write-sample-config
|
|
88
|
+
test -f "$tmp_dir/config/config.toml"
|
|
89
|
+
uvx --from dist/*.whl mcp-sonarcloud \
|
|
90
|
+
--config-dir "$tmp_dir/config" \
|
|
91
|
+
--state-dir "$tmp_dir/state" \
|
|
92
|
+
--cache-dir "$tmp_dir/cache" \
|
|
93
|
+
--print-paths
|
|
94
|
+
|
|
95
|
+
- name: Smoke test sdist
|
|
96
|
+
run: |
|
|
97
|
+
tmp_dir="$(mktemp -d)"
|
|
98
|
+
uvx --from dist/*.tar.gz mcp-sonarcloud \
|
|
99
|
+
--config-dir "$tmp_dir/config" \
|
|
100
|
+
--state-dir "$tmp_dir/state" \
|
|
101
|
+
--cache-dir "$tmp_dir/cache" \
|
|
102
|
+
--write-sample-config
|
|
103
|
+
test -f "$tmp_dir/config/config.toml"
|
|
104
|
+
uvx --from dist/*.tar.gz mcp-sonarcloud \
|
|
105
|
+
--config-dir "$tmp_dir/config" \
|
|
106
|
+
--state-dir "$tmp_dir/state" \
|
|
107
|
+
--cache-dir "$tmp_dir/cache" \
|
|
108
|
+
--print-paths
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
venv/
|
|
25
|
+
ENV/
|
|
26
|
+
env/
|
|
27
|
+
.venv
|
|
28
|
+
|
|
29
|
+
# IDE
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
*~
|
|
35
|
+
|
|
36
|
+
# Testing
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
.coverage
|
|
39
|
+
htmlcov/
|
|
40
|
+
|
|
41
|
+
# Environment variables
|
|
42
|
+
.env
|
|
43
|
+
.env.local
|
|
44
|
+
|
|
45
|
+
# OS
|
|
46
|
+
.DS_Store
|
|
47
|
+
Thumbs.db
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Development Notes
|
|
2
|
+
|
|
3
|
+
## Implementation Details
|
|
4
|
+
|
|
5
|
+
### Architecture
|
|
6
|
+
|
|
7
|
+
This MCP server is built using the FastMCP framework from the MCP Python SDK. The architecture follows a simple, straightforward approach:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/mcp_sonarcloud/
|
|
11
|
+
├── __init__.py # Package initialization
|
|
12
|
+
└── server.py # Main server with all tools
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### API Endpoints Mapping
|
|
16
|
+
|
|
17
|
+
The server implements 15 tools that map to these SonarCloud API endpoints:
|
|
18
|
+
|
|
19
|
+
| Tool Name | API Endpoint | HTTP Method | Purpose |
|
|
20
|
+
|-----------|--------------|-------------|---------|
|
|
21
|
+
| **Components/Projects** | | | |
|
|
22
|
+
| `search_my_sonarqube_projects` | `/api/components/search` | GET | List projects |
|
|
23
|
+
| `show_component` | `/api/components/show` | GET | Show component metadata |
|
|
24
|
+
| `component_tree` | `/api/components/tree` | GET | Traverse component hierarchy |
|
|
25
|
+
| **Issues** | | | |
|
|
26
|
+
| `search_sonar_issues_in_projects` | `/api/issues/search` | GET | Search issues |
|
|
27
|
+
| `list_issue_authors` | `/api/issues/authors` | GET | List issue authors |
|
|
28
|
+
| `get_issue_changelog` | `/api/issues/changelog` | GET | Get issue change history |
|
|
29
|
+
| `list_issue_tags` | `/api/issues/tags` | GET | List issue tags |
|
|
30
|
+
| **Quality Gates** | | | |
|
|
31
|
+
| `get_project_quality_gate_status` | `/api/qualitygates/project_status` | GET | Get QG status |
|
|
32
|
+
| `list_quality_gates` | `/api/qualitygates/list` | GET | List quality gates |
|
|
33
|
+
| `show_quality_gate` | `/api/qualitygates/show` | GET | Show quality gate details |
|
|
34
|
+
| `search_quality_gates` | `/api/qualitygates/search` | GET | Search projects by QG |
|
|
35
|
+
| `get_quality_gate_by_project` | `/api/qualitygates/get_by_project` | GET | Get QG for project |
|
|
36
|
+
| **Security Hotspots** | | | |
|
|
37
|
+
| `search_hotspots` | `/api/hotspots/search` | GET | Search hotspots |
|
|
38
|
+
| `show_hotspot` | `/api/hotspots/show` | GET | Get hotspot details |
|
|
39
|
+
| `change_hotspot_status` | `/api/hotspots/change_status` | POST | Update hotspot status |
|
|
40
|
+
|
|
41
|
+
### Authentication
|
|
42
|
+
|
|
43
|
+
The server uses Bearer token authentication:
|
|
44
|
+
- Token is provided via `SONARCLOUD_TOKEN` environment variable
|
|
45
|
+
- Each request includes: `Authorization: Bearer <token>` header
|
|
46
|
+
- Organization is automatically added to requests if `SONARCLOUD_ORGANIZATION` is set
|
|
47
|
+
|
|
48
|
+
### Design Decisions
|
|
49
|
+
|
|
50
|
+
1. **Single File Implementation**: All code is in one file for simplicity and easy understanding
|
|
51
|
+
2. **Pydantic Models**: Used for type safety and automatic validation of responses
|
|
52
|
+
3. **Pydantic Field Annotations**: All tool parameters use Field() with comprehensive descriptions, valid values, formats, and examples for optimal LLM consumption
|
|
53
|
+
4. **Async/Await**: All API calls are asynchronous for better performance
|
|
54
|
+
5. **Environment-Based Config**: No config files needed, just environment variables
|
|
55
|
+
6. **Structured Responses**: Tools return structured data (Pydantic models or dicts) instead of raw JSON
|
|
56
|
+
7. **LLM-First Documentation**: Every tool includes examples and explicit documentation of valid values to minimize trial-and-error by AI agents
|
|
57
|
+
|
|
58
|
+
### Hotspots API Research
|
|
59
|
+
|
|
60
|
+
The hotspots API was not well-documented publicly, but through research:
|
|
61
|
+
|
|
62
|
+
1. **Source**: Analyzed SonarLint Core Java implementation
|
|
63
|
+
2. **Endpoints Found**:
|
|
64
|
+
- `/api/hotspots/search.protobuf` - Returns protobuf format
|
|
65
|
+
- `/api/hotspots/show.protobuf` - Returns protobuf format
|
|
66
|
+
- `/api/hotspots/change_status` - Form-encoded POST
|
|
67
|
+
- We use the JSON versions (without `.protobuf`)
|
|
68
|
+
|
|
69
|
+
3. **Status Values**:
|
|
70
|
+
- `TO_REVIEW`: Hotspot needs review
|
|
71
|
+
- `REVIEWED`: Hotspot has been reviewed
|
|
72
|
+
|
|
73
|
+
4. **Resolution Values** (when status=REVIEWED):
|
|
74
|
+
- `FIXED`: Vulnerability was fixed
|
|
75
|
+
- `SAFE`: Code is safe, not a vulnerability
|
|
76
|
+
- `ACKNOWLEDGED`: Risk is accepted
|
|
77
|
+
|
|
78
|
+
### Testing Strategy
|
|
79
|
+
|
|
80
|
+
Tests use `pytest-httpx` to mock HTTP requests:
|
|
81
|
+
- Each test mocks the expected API response
|
|
82
|
+
- Tests verify both the request format and response parsing
|
|
83
|
+
- All tests run without requiring actual SonarCloud credentials
|
|
84
|
+
|
|
85
|
+
### Comparison with Official Server
|
|
86
|
+
|
|
87
|
+
| Feature | This Implementation | Official SonarSource Server |
|
|
88
|
+
|---------|-------------------|---------------------------|
|
|
89
|
+
| Language | Python | Kotlin/Java |
|
|
90
|
+
| Lines of Code | ~730 | ~5000+ |
|
|
91
|
+
| Tools Count | 15 tools | ~10 tools |
|
|
92
|
+
| Hotspot Support | ✅ Full (search, show, change status) | ❌ Missing |
|
|
93
|
+
| Quality Gates | ✅ Full (5 tools) | ⚠️ Partial |
|
|
94
|
+
| Issues API | ✅ Good (4 tools) | ✅ Yes |
|
|
95
|
+
| LLM Documentation | ✅ Comprehensive Field annotations | ⚠️ Basic |
|
|
96
|
+
| Code Analysis | ❌ No | ✅ Yes |
|
|
97
|
+
| Dependency Risks | ❌ No | ✅ Yes |
|
|
98
|
+
| Metrics Search | ❌ No | ✅ Yes |
|
|
99
|
+
| Ease of Modification | ✅ Very Easy | ⚠️ Complex |
|
|
100
|
+
|
|
101
|
+
### Future Enhancements
|
|
102
|
+
|
|
103
|
+
Potential additions:
|
|
104
|
+
1. Add caching for frequently accessed data
|
|
105
|
+
2. Support batch operations on hotspots
|
|
106
|
+
3. Add more filtering options for issues/hotspots
|
|
107
|
+
4. Support for SonarQube Server (not just SonarCloud)
|
|
108
|
+
5. Add telemetry/metrics collection
|
|
109
|
+
6. Support for SSE transport in addition to stdio
|
|
110
|
+
|
|
111
|
+
### Known Limitations
|
|
112
|
+
|
|
113
|
+
1. **No Pagination Automation**: Users must manually page through large result sets
|
|
114
|
+
2. **Organization Required**: Works best with SonarCloud organizations
|
|
115
|
+
3. **No Rate Limiting**: No built-in rate limiting protection
|
|
116
|
+
4. **Limited Error Context**: HTTP errors could be more descriptive
|
|
117
|
+
5. **No Retry Logic**: Failed requests are not automatically retried
|
|
118
|
+
|
|
119
|
+
### Contributing Guidelines
|
|
120
|
+
|
|
121
|
+
When adding new tools:
|
|
122
|
+
|
|
123
|
+
1. Add the tool function with `@mcp.tool()` decorator
|
|
124
|
+
2. Use type hints and Pydantic `Field()` annotations for all parameters
|
|
125
|
+
3. Document valid values, formats, and provide examples in Field descriptions
|
|
126
|
+
4. Create a Pydantic model for structured responses if needed
|
|
127
|
+
5. Add comprehensive docstring with examples and parameter relationships
|
|
128
|
+
6. Write tests for the new tool in `tests/test_server.py`
|
|
129
|
+
7. Update README.md and SONARCLOUD_API_SUPPORT.md
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
```python
|
|
133
|
+
@mcp.tool()
|
|
134
|
+
async def new_tool(
|
|
135
|
+
param: str = Field(
|
|
136
|
+
description="Parameter description with example (e.g., 'my-value')"
|
|
137
|
+
),
|
|
138
|
+
status: str = Field(
|
|
139
|
+
description="Status value. Valid values: 'OPEN', 'CLOSED', 'PENDING'"
|
|
140
|
+
),
|
|
141
|
+
optional_param: Optional[int] = Field(
|
|
142
|
+
default=None,
|
|
143
|
+
description="Optional parameter (e.g., 123)"
|
|
144
|
+
),
|
|
145
|
+
) -> ResponseModel:
|
|
146
|
+
"""Tool description explaining what it does and when to use it.
|
|
147
|
+
|
|
148
|
+
Provide context about the tool's purpose, what data it returns,
|
|
149
|
+
and any important constraints or requirements.
|
|
150
|
+
|
|
151
|
+
Example: new_tool(param="my-project", status="OPEN", optional_param=100)
|
|
152
|
+
"""
|
|
153
|
+
result = await make_request("/api/endpoint", params={"param": param})
|
|
154
|
+
return ResponseModel(**result)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Key Documentation Requirements:**
|
|
158
|
+
- Use `Field()` for ALL parameters (including required ones)
|
|
159
|
+
- Document valid enum values explicitly (e.g., "Valid values: 'X', 'Y', 'Z'")
|
|
160
|
+
- Specify format for complex inputs (e.g., "comma-separated list")
|
|
161
|
+
- Include concrete examples in descriptions (e.g., "e.g., 'my-project'")
|
|
162
|
+
- Add usage example in docstring showing realistic parameters
|
|
163
|
+
- Explain parameter relationships (e.g., "required when X is Y")
|
|
164
|
+
- State what the tool returns in the docstring
|
|
165
|
+
|
|
166
|
+
## Debugging
|
|
167
|
+
|
|
168
|
+
To debug the server:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Enable verbose logging
|
|
172
|
+
export SONARCLOUD_TOKEN=your-token
|
|
173
|
+
export SONARCLOUD_ORGANIZATION=your-org
|
|
174
|
+
|
|
175
|
+
# Run with MCP inspector for interactive debugging
|
|
176
|
+
uvx mcp-inspector uv run mcp-sonarcloud
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Performance Considerations
|
|
180
|
+
|
|
181
|
+
- All API calls use httpx's async client for concurrency
|
|
182
|
+
- Connection pooling is handled by httpx automatically
|
|
183
|
+
- Timeout is set to 30 seconds for all requests
|
|
184
|
+
- Consider implementing caching for frequently accessed data
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Lukas Lehner <lehner.lukas@gmail.com>
|
|
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.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|