# Directory Structure ``` ├── .github │ ├── actions │ │ └── setup-python-env │ │ └── action.yml │ └── workflows │ ├── main.yml │ ├── on-release-main.yml │ └── validate-codecov-config.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── .vscode │ └── settings.json ├── assets │ ├── config-cursor.png │ └── showcase.jpeg ├── codecov.yaml ├── CONTRIBUTING.md ├── Dockerfile ├── examples │ └── index.html ├── LICENSE ├── Makefile ├── pyproject.toml ├── pytest.ini ├── README.md ├── tests │ └── test_hello.py ├── tox.ini ├── uv.lock └── yourware_mcp ├── __init__.py ├── app.py ├── cli.py ├── client.py ├── credentials.py ├── log.py └── utils.py ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 1 | 3.12 2 | ``` -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- ```yaml 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: "v5.0.0" 4 | hooks: 5 | - id: check-case-conflict 6 | - id: check-merge-conflict 7 | - id: check-toml 8 | - id: check-yaml 9 | - id: check-json 10 | exclude: ^.devcontainer/devcontainer.json 11 | - id: pretty-format-json 12 | exclude: ^.devcontainer/devcontainer.json 13 | args: [--autofix] 14 | - id: end-of-file-fixer 15 | - id: trailing-whitespace 16 | - repo: https://github.com/executablebooks/mdformat 17 | rev: 0.7.22 18 | hooks: 19 | - id: mdformat 20 | additional_dependencies: 21 | [mdformat-gfm, mdformat-frontmatter, mdformat-footnote] 22 | - repo: https://github.com/astral-sh/ruff-pre-commit 23 | rev: "v0.11.9" 24 | hooks: 25 | - id: ruff 26 | args: [--exit-non-zero-on-fix] 27 | - id: ruff-format 28 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | docs/source 2 | 3 | # From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 90 | __pypackages__/ 91 | 92 | # Celery stuff 93 | celerybeat-schedule 94 | celerybeat.pid 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | 126 | # pytype static type analyzer 127 | .pytype/ 128 | 129 | # Cython debug symbols 130 | cython_debug/ 131 | 132 | # Vscode config files 133 | # .vscode/ 134 | 135 | # PyCharm 136 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 137 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 138 | # and can be added to the global gitignore or merged into this file. For a more nuclear 139 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 140 | #.idea/ 141 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | [](https://img.shields.io/github/v/release/ai-zerolab/yourware-mcp) 2 | [](https://github.com/ai-zerolab/yourware-mcp/actions/workflows/main.yml?query=branch%3Amain) 3 | [](https://img.shields.io/github/commit-activity/m/ai-zerolab/yourware-mcp) 4 | [](https://img.shields.io/github/license/ai-zerolab/yourware-mcp) 5 | 6 | <!-- [](https://codecov.io/gh/ai-zerolab/yourware-mcp) --> 7 | 8 | # Yourware MCP 9 | 10 | MCP server to upload your project to [yourware](https://www.yourware.so). Support single file or directory. 11 | 12 | ## Showcase 13 | 14 | Visit on [yourware](https://v9gfmmif5s.app.yourware.so/): https://v9gfmmif5s.app.yourware.so/ 15 | 16 |  17 | 18 | ## Pre-requisites 19 | 20 | 1. You need to login to [yourware](https://www.yourware.so) 21 | 1. Then you can create a new API key, and set the `YOURWARE_API_KEY` environment variable. Don't worry, you chat with LLM to create and store the API key. 22 | 23 | ## Configuration 24 | 25 | ### General configuration 26 | 27 | You can use the following configuration for cline/cursor/windsurf... 28 | 29 | ```json 30 | { 31 | "mcpServers": { 32 | "yourware-mcp": { 33 | "command": "uvx", 34 | "args": ["yourware-mcp@latest", "stdio"], 35 | "env": {} 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | ### Cursor config guide 42 | 43 | In cursor settings -> features -> MCP Servers, Add a new MCP Server, name it `yourware-mcp` and set the command to `uvx yourware-mcp@latest stdio` 44 | 45 |  46 | 47 | ### Config claude code 48 | 49 | ```bash 50 | claude mcp add yourware-mcp -s user -- uvx yourware-mcp@latest stdio 51 | ``` 52 | 53 | ## Available environments variables 54 | 55 | `YOURWARE_API_KEY` for the API key, you can also let llm config it for you. 56 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | # Contributing to `yourware-mcp` 2 | 3 | Contributions are welcome, and they are greatly appreciated! 4 | Every little bit helps, and credit will always be given. 5 | 6 | You can contribute in many ways: 7 | 8 | # Types of Contributions 9 | 10 | ## Report Bugs 11 | 12 | Report bugs at https://github.com/ai-zerolab/yourware-mcp/issues 13 | 14 | If you are reporting a bug, please include: 15 | 16 | - Your operating system name and version. 17 | - Any details about your local setup that might be helpful in troubleshooting. 18 | - Detailed steps to reproduce the bug. 19 | 20 | ## Fix Bugs 21 | 22 | Look through the GitHub issues for bugs. 23 | Anything tagged with "bug" and "help wanted" is open to whoever wants to implement a fix for it. 24 | 25 | ## Implement Features 26 | 27 | Look through the GitHub issues for features. 28 | Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. 29 | 30 | ## Write Documentation 31 | 32 | yourware-mcp could always use more documentation, whether as part of the official docs, in docstrings, or even on the web in blog posts, articles, and such. 33 | 34 | ## Submit Feedback 35 | 36 | The best way to send feedback is to file an issue at https://github.com/ai-zerolab/yourware-mcp/issues. 37 | 38 | If you are proposing a new feature: 39 | 40 | - Explain in detail how it would work. 41 | - Keep the scope as narrow as possible, to make it easier to implement. 42 | - Remember that this is a volunteer-driven project, and that contributions 43 | are welcome :) 44 | 45 | # Get Started! 46 | 47 | Ready to contribute? Here's how to set up `yourware-mcp` for local development. 48 | Please note this documentation assumes you already have `uv` and `Git` installed and ready to go. 49 | 50 | 1. Fork the `yourware-mcp` repo on GitHub. 51 | 52 | 1. Clone your fork locally: 53 | 54 | ```bash 55 | cd <directory_in_which_repo_should_be_created> 56 | git clone [email protected]:YOUR_NAME/yourware-mcp.git 57 | ``` 58 | 59 | 3. Now we need to install the environment. Navigate into the directory 60 | 61 | ```bash 62 | cd yourware-mcp 63 | ``` 64 | 65 | Then, install and activate the environment with: 66 | 67 | ```bash 68 | uv sync 69 | ``` 70 | 71 | 4. Install pre-commit to run linters/formatters at commit time: 72 | 73 | ```bash 74 | uv run pre-commit install 75 | ``` 76 | 77 | 5. Create a branch for local development: 78 | 79 | ```bash 80 | git checkout -b name-of-your-bugfix-or-feature 81 | ``` 82 | 83 | Now you can make your changes locally. 84 | 85 | 6. Don't forget to add test cases for your added functionality to the `tests` directory. 86 | 87 | 1. When you're done making changes, check that your changes pass the formatting tests. 88 | 89 | ```bash 90 | make check 91 | ``` 92 | 93 | Now, validate that all unit tests are passing: 94 | 95 | ```bash 96 | make test 97 | ``` 98 | 99 | 9. Before raising a pull request you should also run tox. 100 | This will run the tests across different versions of Python: 101 | 102 | ```bash 103 | tox 104 | ``` 105 | 106 | This requires you to have multiple versions of python installed. 107 | This step is also triggered in the CI/CD pipeline, so you could also choose to skip this step locally. 108 | 109 | 10. Commit your changes and push your branch to GitHub: 110 | 111 | ```bash 112 | git add . 113 | git commit -m "Your detailed description of your changes." 114 | git push origin name-of-your-bugfix-or-feature 115 | ``` 116 | 117 | 11. Submit a pull request through the GitHub website. 118 | 119 | # Pull Request Guidelines 120 | 121 | Before you submit a pull request, check that it meets these guidelines: 122 | 123 | 1. The pull request should include tests. 124 | 125 | 1. If the pull request adds functionality, the docs should be updated. 126 | Put your new functionality into a function with a docstring, and add the feature to the list in `README.md`. 127 | ``` -------------------------------------------------------------------------------- /yourware_mcp/__init__.py: -------------------------------------------------------------------------------- ```python 1 | ``` -------------------------------------------------------------------------------- /tests/test_hello.py: -------------------------------------------------------------------------------- ```python 1 | def test_hello(): 2 | assert 1 == 1 3 | ``` -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- ``` 1 | # pytest.ini 2 | [pytest] 3 | asyncio_mode = auto 4 | ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python" 3 | } 4 | ``` -------------------------------------------------------------------------------- /yourware_mcp/utils.py: -------------------------------------------------------------------------------- ```python 1 | def urljoin(base: str, url: str) -> str: 2 | return "/".join([base.rstrip("/"), url.lstrip("/")]) 3 | ``` -------------------------------------------------------------------------------- /yourware_mcp/log.py: -------------------------------------------------------------------------------- ```python 1 | import os 2 | 3 | USER_DEFINED_LOG_LEVEL = os.getenv("YOURWARE_MCP_LOG_LEVEL", "INFO") 4 | 5 | os.environ["LOGURU_LEVEL"] = USER_DEFINED_LOG_LEVEL 6 | 7 | from loguru import logger # noqa: E402 8 | 9 | __all__ = ["logger"] 10 | ``` -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- ```yaml 1 | coverage: 2 | range: 70..100 3 | round: down 4 | precision: 1 5 | status: 6 | project: 7 | default: 8 | target: 90% 9 | threshold: 0.5% 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 0% 14 | informational: true 15 | codecov: 16 | token: f927bff4-d404-4986-8c11-624eadda8431 17 | ``` -------------------------------------------------------------------------------- /yourware_mcp/cli.py: -------------------------------------------------------------------------------- ```python 1 | import typer 2 | 3 | from yourware_mcp.app import mcp 4 | 5 | app = typer.Typer() 6 | 7 | 8 | @app.command() 9 | def stdio(): 10 | mcp.run(transport="stdio") 11 | 12 | 13 | @app.command() 14 | def sse( 15 | host: str = "localhost", 16 | port: int = 9123, 17 | ): 18 | mcp.settings.host = host 19 | mcp.settings.port = port 20 | mcp.run(transport="sse") 21 | 22 | 23 | if __name__ == "__main__": 24 | app(["stdio"]) 25 | ``` -------------------------------------------------------------------------------- /.github/workflows/validate-codecov-config.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: validate-codecov-config 2 | 3 | on: 4 | pull_request: 5 | paths: [codecov.yaml] 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | validate-codecov-config: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Validate codecov configuration 15 | run: curl -sSL --fail-with-body --data-binary @codecov.yaml https://codecov.io/validate 16 | ``` -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- ``` 1 | [tox] 2 | skipsdist = true 3 | envlist = py39, py310, py311, py312, py313 4 | 5 | [gh-actions] 6 | python = 7 | 3.10: py310 8 | 3.11: py311 9 | 3.12: py312 10 | 3.13: py313 11 | 12 | [testenv] 13 | passenv = PYTHON_VERSION 14 | allowlist_externals = uv 15 | commands = 16 | uv sync --python {envpython} 17 | uv run python -m pytest --doctest-modules tests --cov --cov-config=pyproject.toml --cov-report=xml 18 | ``` -------------------------------------------------------------------------------- /yourware_mcp/client.py: -------------------------------------------------------------------------------- ```python 1 | from __future__ import annotations 2 | 3 | from functools import cache 4 | from typing import TYPE_CHECKING 5 | 6 | import httpx 7 | 8 | if TYPE_CHECKING: 9 | from yourware_mcp.credentials import Credentials 10 | 11 | 12 | @cache 13 | def get_client(credentials: Credentials) -> httpx.AsyncClient: 14 | return httpx.AsyncClient(base_url=credentials.base_url, headers={"Authorization": f"Bearer {credentials.api_key}"}) 15 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Install uv 2 | FROM python:3.12-slim 3 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv 4 | 5 | # Change the working directory to the `app` directory 6 | WORKDIR /app 7 | 8 | # Copy the lockfile and `pyproject.toml` into the image 9 | COPY uv.lock /app/uv.lock 10 | COPY pyproject.toml /app/pyproject.toml 11 | 12 | # Install dependencies 13 | RUN uv sync --frozen --no-install-project 14 | 15 | # Copy the project into the image 16 | COPY . /app 17 | 18 | # Sync the project 19 | RUN uv sync --frozen 20 | 21 | CMD [ "python", "yourware_mcp/foo.py" ] 22 | ``` -------------------------------------------------------------------------------- /.github/actions/setup-python-env/action.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: "Setup Python Environment" 2 | description: "Set up Python environment for the given Python version" 3 | 4 | inputs: 5 | python-version: 6 | description: "Python version to use" 7 | required: true 8 | default: "3.12" 9 | uv-version: 10 | description: "uv version to use" 11 | required: true 12 | default: "0.6.2" 13 | 14 | runs: 15 | using: "composite" 16 | steps: 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ inputs.python-version }} 20 | 21 | - name: Install uv 22 | uses: astral-sh/setup-uv@v5 23 | with: 24 | version: ${{ inputs.uv-version }} 25 | enable-cache: 'true' 26 | cache-suffix: ${{ matrix.python-version }} 27 | 28 | - name: Install Python dependencies 29 | run: uv sync --frozen 30 | shell: bash 31 | ``` -------------------------------------------------------------------------------- /yourware_mcp/credentials.py: -------------------------------------------------------------------------------- ```python 1 | from __future__ import annotations 2 | 3 | import os 4 | from pathlib import Path 5 | 6 | from pydantic import BaseModel, ConfigDict 7 | 8 | from yourware_mcp.client import get_client 9 | 10 | CREDENTIALS_PATH = Path("~/.yourware/credentials.json").expanduser().resolve() 11 | API_BASE_URL = os.getenv("YOURWARE_ENDPOINT", "https://www.yourware.so") 12 | 13 | 14 | class Credentials(BaseModel): 15 | api_key: str 16 | base_url: str = API_BASE_URL 17 | 18 | model_config = ConfigDict(frozen=True) 19 | 20 | def store_credentials(self) -> None: 21 | CREDENTIALS_PATH.parent.mkdir(parents=True, exist_ok=True) 22 | CREDENTIALS_PATH.write_text(self.model_dump_json(include=["api_key"])) 23 | 24 | @classmethod 25 | def load(cls) -> Credentials: 26 | api_key_from_env = os.getenv("YOURWARE_API_KEY") 27 | if api_key_from_env: 28 | return cls(api_key=api_key_from_env) 29 | 30 | if not CREDENTIALS_PATH.exists(): 31 | raise FileNotFoundError(f"Credentials not found at {CREDENTIALS_PATH}") # noqa: TRY003 32 | 33 | return cls.model_validate_json(CREDENTIALS_PATH.read_text()) 34 | 35 | async def check_credentials(self) -> bool: 36 | api_key_list_path = "/api/v1/api-keys/list" 37 | client = get_client(self) 38 | response = await client.get(api_key_list_path) 39 | return response.status_code == 200 40 | ``` -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened, ready_for_review] 9 | 10 | jobs: 11 | quality: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out 15 | uses: actions/checkout@v4 16 | 17 | - uses: actions/cache@v4 18 | with: 19 | path: ~/.cache/pre-commit 20 | key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 21 | 22 | - name: Set up the environment 23 | uses: ./.github/actions/setup-python-env 24 | 25 | - name: Run checks 26 | run: make check 27 | 28 | tests-and-type-check: 29 | runs-on: ubuntu-latest 30 | strategy: 31 | matrix: 32 | python-version: ["3.10", "3.11", "3.12", "3.13"] 33 | fail-fast: false 34 | env: 35 | UV_PYTHON: ${{ inputs.python-version }} 36 | defaults: 37 | run: 38 | shell: bash 39 | steps: 40 | - name: Check out 41 | uses: actions/checkout@v4 42 | 43 | - name: Set up the environment 44 | uses: ./.github/actions/setup-python-env 45 | with: 46 | python-version: ${{ matrix.python-version }} 47 | 48 | - name: Run tests 49 | run: uv run python -m pytest tests --cov --cov-config=pyproject.toml --cov-report=xml 50 | 51 | - name: Upload coverage reports to Codecov with GitHub Action on Python 3.11 52 | uses: codecov/codecov-action@v4 53 | if: ${{ matrix.python-version == '3.11' }} 54 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "yourware-mcp" 3 | version = "0.0.1" 4 | description = "yourware mcp server" 5 | authors = [{ name = "Wh1isper", email = "[email protected]" }] 6 | readme = "README.md" 7 | keywords = ['python'] 8 | requires-python = ">=3.10,<4.0" 9 | classifiers = [ 10 | "Intended Audience :: Developers", 11 | "Programming Language :: Python", 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.10", 14 | "Programming Language :: Python :: 3.11", 15 | "Programming Language :: Python :: 3.12", 16 | "Programming Language :: Python :: 3.13", 17 | "Topic :: Software Development :: Libraries :: Python Modules", 18 | ] 19 | dependencies = [ 20 | "httpx>=0.28.1", 21 | "loguru>=0.7.3", 22 | "mcp[cli]>=1.6.0", 23 | "pydantic>=2.11.1", 24 | "typer>=0.15.2", 25 | ] 26 | 27 | [project.urls] 28 | Homepage = "https://ai-zerolab.github.io/yourware-mcp/" 29 | Repository = "https://github.com/ai-zerolab/yourware-mcp" 30 | Documentation = "https://ai-zerolab.github.io/yourware-mcp/" 31 | 32 | [dependency-groups] 33 | dev = [ 34 | "pytest>=7.2.0", 35 | "pre-commit>=2.20.0", 36 | "pytest-asyncio>=0.25.3", 37 | "tox-uv>=1.11.3", 38 | "deptry>=0.22.0", 39 | "pytest-cov>=4.0.0", 40 | "ruff>=0.9.2", 41 | 42 | ] 43 | 44 | [project.scripts] 45 | yourware-mcp = "yourware_mcp.cli:app" 46 | 47 | 48 | [build-system] 49 | requires = ["hatchling"] 50 | build-backend = "hatchling.build" 51 | 52 | [tool.setuptools] 53 | py-modules = ["yourware_mcp"] 54 | 55 | [tool.pytest.ini_options] 56 | testpaths = ["tests"] 57 | 58 | [tool.ruff] 59 | target-version = "py39" 60 | line-length = 120 61 | fix = true 62 | 63 | [tool.ruff.lint] 64 | select = [ 65 | # flake8-2020 66 | "YTT", 67 | # flake8-bandit 68 | "S", 69 | # flake8-bugbear 70 | "B", 71 | # flake8-builtins 72 | "A", 73 | # flake8-comprehensions 74 | "C4", 75 | # flake8-debugger 76 | "T10", 77 | # flake8-simplify 78 | "SIM", 79 | # isort 80 | "I", 81 | # mccabe 82 | "C90", 83 | # pycodestyle 84 | "E", 85 | "W", 86 | # pyflakes 87 | "F", 88 | # pygrep-hooks 89 | "PGH", 90 | # pyupgrade 91 | "UP", 92 | # ruff 93 | "RUF", 94 | # tryceratops 95 | "TRY", 96 | ] 97 | ignore = [ 98 | # LineTooLong 99 | "E501", 100 | # DoNotAssignLambda 101 | "E731", 102 | ] 103 | 104 | [tool.ruff.lint.per-file-ignores] 105 | "tests/*" = ["S101"] 106 | 107 | [tool.ruff.format] 108 | preview = true 109 | 110 | [tool.coverage.report] 111 | skip_empty = true 112 | 113 | [tool.coverage.run] 114 | branch = true 115 | source = ["yourware_mcp"] 116 | ``` -------------------------------------------------------------------------------- /.github/workflows/on-release-main.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: release-main 2 | 3 | permissions: 4 | contents: write 5 | packages: write 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | set-version: 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Export tag 18 | id: vars 19 | run: echo tag=${GITHUB_REF#refs/*/} >> $GITHUB_OUTPUT 20 | if: ${{ github.event_name == 'release' }} 21 | 22 | - name: Update project version 23 | run: | 24 | sed -i "s/^version = \".*\"/version = \"$RELEASE_VERSION\"/" pyproject.toml 25 | env: 26 | RELEASE_VERSION: ${{ steps.vars.outputs.tag }} 27 | if: ${{ github.event_name == 'release' }} 28 | 29 | - name: Upload updated pyproject.toml 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: pyproject-toml 33 | path: pyproject.toml 34 | 35 | publish: 36 | runs-on: ubuntu-latest 37 | needs: [set-version] 38 | steps: 39 | - name: Check out 40 | uses: actions/checkout@v4 41 | 42 | - name: Set up the environment 43 | uses: ./.github/actions/setup-python-env 44 | 45 | - name: Download updated pyproject.toml 46 | uses: actions/download-artifact@v4 47 | with: 48 | name: pyproject-toml 49 | 50 | - name: Build package 51 | run: uv build 52 | 53 | - name: Publish package 54 | run: uv publish 55 | env: 56 | UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} 57 | 58 | - name: Upload dists to release 59 | uses: svenstaro/upload-release-action@v2 60 | with: 61 | repo_token: ${{ secrets.GITHUB_TOKEN }} 62 | file: dist/* 63 | file_glob: true 64 | tag: ${{ github.ref }} 65 | overwrite: true 66 | 67 | 68 | push-image: 69 | runs-on: ubuntu-latest 70 | needs: [set-version] 71 | steps: 72 | - uses: actions/checkout@v4 73 | - name: Export tag 74 | id: vars 75 | run: echo tag=${GITHUB_REF#refs/*/} >> $GITHUB_OUTPUT 76 | if: ${{ github.event_name == 'release' }} 77 | - name: Set up QEMU 78 | uses: docker/setup-qemu-action@v3 79 | - name: Set up Docker Buildx 80 | uses: docker/setup-buildx-action@v3 81 | - name: Login to Github Container Registry 82 | uses: docker/login-action@v3 83 | with: 84 | registry: ghcr.io 85 | username: ai-zerolab 86 | password: ${{ secrets.GITHUB_TOKEN }} 87 | - name: Build and push image 88 | id: docker_build_publish 89 | uses: docker/build-push-action@v5 90 | with: 91 | context: . 92 | platforms: linux/amd64,linux/arm64/v8 93 | cache-from: type=gha 94 | cache-to: type=gha,mode=max 95 | file: ./Dockerfile 96 | push: true 97 | tags: | 98 | ghcr.io/ai-zerolab/yourware-mcp:${{ steps.vars.outputs.tag }} 99 | ghcr.io/ai-zerolab/yourware-mcp:latest 100 | ``` -------------------------------------------------------------------------------- /yourware_mcp/app.py: -------------------------------------------------------------------------------- ```python 1 | import io 2 | import os 3 | import zipfile 4 | from pathlib import Path 5 | from typing import Annotated 6 | 7 | import httpx 8 | from mcp.server.fastmcp import FastMCP 9 | 10 | from yourware_mcp.client import get_client 11 | from yourware_mcp.credentials import API_BASE_URL, CREDENTIALS_PATH, Credentials 12 | from yourware_mcp.utils import urljoin 13 | 14 | mcp = FastMCP("yourware-mcp") 15 | 16 | 17 | @mcp.tool(description="Check your yourware credentials exists and are valid.") 18 | async def check_credentials(): 19 | try: 20 | credentials = Credentials.load() 21 | except FileNotFoundError: 22 | return { 23 | "success": False, 24 | "message": "Credentials not found", 25 | "help": "Run `create_api_key` to create one", 26 | } 27 | 28 | if not await credentials.check_credentials(): 29 | return { 30 | "success": False, 31 | "message": "Credentials are invalid", 32 | "help": "Call `create_api_key` to create one", 33 | } 34 | 35 | return { 36 | "success": True, 37 | "message": "Credentials are valid", 38 | } 39 | 40 | 41 | @mcp.tool( 42 | description=f"Create a new yourware API key. This will automatically be stored in {CREDENTIALS_PATH.as_posix()}. Use this tool if current credentials are invalid" 43 | ) 44 | async def create_api_key(api_key: Annotated[str | None, "The API key to store"] = None): 45 | if not api_key: 46 | quick_create_address = urljoin(API_BASE_URL, "/api/v1/api-keys/quick-create") 47 | login_address = urljoin(API_BASE_URL, "/login") 48 | return { 49 | "success": False, 50 | "message": "API key is required, please guide the user to create one. Let the user tell you what the page shows and guide them through the login process if needed.", 51 | "help": f"Click this link to create one: {quick_create_address}\n\nClick this link to login if needed: {login_address}", 52 | } 53 | 54 | Credentials(api_key=api_key).store_credentials() 55 | return { 56 | "success": True, 57 | "message": "API key created", 58 | } 59 | 60 | 61 | @mcp.tool( 62 | description="Upload a file or directory to yourware, might be a dist/out directory or a single html file. Use absolute path if possible. " 63 | "For multiple files, you should move them to a directory first, then use this tool to upload the directory" 64 | ) 65 | async def upload_project( # noqa: C901 66 | file_path: Annotated[ 67 | str, 68 | "The path to the dist/out directory or single file. If ends with /, it will be treated as a directory", 69 | ], 70 | cwd: Annotated[ 71 | str | None, 72 | "The current working directory to resolve relative paths from, should be a absolute path", 73 | ] = None, 74 | ): 75 | if cwd: 76 | cwd_path = Path(cwd).expanduser().resolve() 77 | file_path = cwd_path / file_path 78 | else: 79 | file_path = Path(file_path) 80 | 81 | file_path = file_path.expanduser().resolve() 82 | 83 | try: 84 | credentials = Credentials.load() 85 | except FileNotFoundError: 86 | return { 87 | "success": False, 88 | "message": "Credentials not found", 89 | "help": "Run `create_api_key` to create one", 90 | } 91 | 92 | if not await credentials.check_credentials(): 93 | return { 94 | "success": False, 95 | "message": "Credentials are invalid", 96 | "help": "Call `create_api_key` to create one", 97 | } 98 | client = get_client(credentials) 99 | 100 | # 1. Create a zip in memory 101 | zip_buffer = io.BytesIO() 102 | with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: 103 | if file_path.is_dir(): 104 | # 2. Zip the directory into it 105 | for root, dirs, files in os.walk(file_path): 106 | # Skip .git directories 107 | if ".git" in dirs: 108 | dirs.remove(".git") # This modifies dirs in-place to prevent os.walk from traversing .git 109 | 110 | for file in files: 111 | file_full_path = Path(root) / file 112 | arc_name = file_full_path.relative_to(file_path) 113 | zip_file.write(file_full_path, arcname=arc_name) 114 | else: 115 | # Zip the single file 116 | zip_file.write(file_path, arcname=file_path.name) 117 | 118 | # Get the zip content 119 | zip_buffer.seek(0) 120 | zip_content = zip_buffer.getvalue() 121 | zip_size = len(zip_content) 122 | 123 | # 3. Call /api/v1/files/upload for upload infos 124 | upload_response = await client.post( 125 | "/api/v1/files/upload", 126 | json={ 127 | "files": [ 128 | { 129 | "file_name": "source_code.zip", 130 | "file_size": zip_size, 131 | "mime_type": "application/zip", 132 | } 133 | ], 134 | "event_type": "source_code", 135 | "is_public": False, 136 | }, 137 | ) 138 | 139 | if upload_response.status_code != 200: 140 | return { 141 | "success": False, 142 | "message": f"Failed to get upload info: {upload_response.text}", 143 | } 144 | 145 | upload_data = upload_response.json() 146 | upload_info = upload_data["data"]["upload_infos"][0] 147 | file_id = upload_info["file_id"] 148 | upload_url = upload_info["upload_url"] 149 | fields = upload_info["fields"] 150 | 151 | # 4. Upload the zip to the upload url 152 | files = {"file": ("source_code.zip", zip_content, "application/zip")} 153 | form_data = {**fields} 154 | 155 | async with httpx.AsyncClient() as upload_client: 156 | upload_result = await upload_client.post(upload_url, data=form_data, files=files) 157 | 158 | if upload_result.status_code not in (200, 201, 204): 159 | return { 160 | "success": False, 161 | "message": f"Failed to upload file: {upload_result.text}", 162 | } 163 | 164 | # 5. Call /api/v1/projects/deploy with the file_id 165 | deploy_response = await client.post("/api/v1/projects/deploy", json={"file_id": file_id}) 166 | 167 | if deploy_response.status_code != 200: 168 | return { 169 | "success": False, 170 | "message": f"Failed to deploy project: {deploy_response.text}", 171 | } 172 | 173 | deploy_data = deploy_response.json() 174 | project_data = deploy_data["data"] 175 | 176 | return { 177 | "success": True, 178 | "message": "Project uploaded successfully", 179 | "project_url": project_data["project_url"], 180 | "iframe_url": project_data["iframe_url"], 181 | } 182 | ``` -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 | <title>Yourware MCP - Upload Your Projects to Yourware</title> 7 | <style> 8 | :root { 9 | --primary-color: #4f46e5; 10 | --secondary-color: #818cf8; 11 | --text-color: #1f2937; 12 | --bg-color: #ffffff; 13 | --card-bg: #f9fafb; 14 | --border-color: #e5e7eb; 15 | --code-bg: #f3f4f6; 16 | } 17 | 18 | body { 19 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 20 | line-height: 1.6; 21 | color: var(--text-color); 22 | background-color: var(--bg-color); 23 | margin: 0; 24 | padding: 0; 25 | } 26 | 27 | .container { 28 | max-width: 1200px; 29 | margin: 0 auto; 30 | padding: 0 20px; 31 | } 32 | 33 | header { 34 | background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); 35 | color: white; 36 | padding: 60px 0; 37 | text-align: center; 38 | } 39 | 40 | header h1 { 41 | margin: 0; 42 | font-size: 3rem; 43 | letter-spacing: -0.5px; 44 | } 45 | 46 | header p { 47 | font-size: 1.2rem; 48 | margin: 15px 0 0; 49 | opacity: 0.9; 50 | } 51 | 52 | section { 53 | padding: 60px 0; 54 | border-bottom: 1px solid var(--border-color); 55 | } 56 | 57 | h2 { 58 | font-size: 2rem; 59 | margin-top: 0; 60 | color: var(--primary-color); 61 | } 62 | 63 | h3 { 64 | font-size: 1.5rem; 65 | margin-top: 30px; 66 | color: var(--primary-color); 67 | } 68 | 69 | .features { 70 | display: grid; 71 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 72 | gap: 30px; 73 | margin-top: 40px; 74 | } 75 | 76 | .feature-card { 77 | background-color: var(--card-bg); 78 | border-radius: 8px; 79 | padding: 25px; 80 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); 81 | transition: transform 0.3s ease; 82 | } 83 | 84 | .feature-card:hover { 85 | transform: translateY(-5px); 86 | } 87 | 88 | .feature-card h3 { 89 | margin-top: 0; 90 | font-size: 1.3rem; 91 | } 92 | 93 | code { 94 | background-color: var(--code-bg); 95 | padding: 2px 5px; 96 | border-radius: 4px; 97 | font-family: 'Courier New', Courier, monospace; 98 | font-size: 0.9em; 99 | } 100 | 101 | pre { 102 | background-color: var(--code-bg); 103 | padding: 15px; 104 | border-radius: 8px; 105 | overflow-x: auto; 106 | font-family: 'Courier New', Courier, monospace; 107 | font-size: 0.9em; 108 | line-height: 1.4; 109 | } 110 | 111 | .btn { 112 | display: inline-block; 113 | background-color: var(--primary-color); 114 | color: white; 115 | padding: 12px 24px; 116 | border-radius: 6px; 117 | text-decoration: none; 118 | font-weight: 600; 119 | transition: background-color 0.3s ease; 120 | } 121 | 122 | .btn:hover { 123 | background-color: var(--secondary-color); 124 | } 125 | 126 | .installation-steps { 127 | counter-reset: step; 128 | list-style-type: none; 129 | padding-left: 0; 130 | } 131 | 132 | .installation-steps li { 133 | position: relative; 134 | margin-bottom: 20px; 135 | padding-left: 50px; 136 | } 137 | 138 | .installation-steps li::before { 139 | counter-increment: step; 140 | content: counter(step); 141 | position: absolute; 142 | left: 0; 143 | top: 0; 144 | background-color: var(--primary-color); 145 | color: white; 146 | width: 30px; 147 | height: 30px; 148 | border-radius: 50%; 149 | display: flex; 150 | align-items: center; 151 | justify-content: center; 152 | font-weight: bold; 153 | } 154 | 155 | .resources { 156 | display: grid; 157 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 158 | gap: 20px; 159 | } 160 | 161 | .resource-card { 162 | background-color: var(--card-bg); 163 | border-radius: 8px; 164 | padding: 20px; 165 | text-align: center; 166 | } 167 | 168 | footer { 169 | text-align: center; 170 | padding: 30px 0; 171 | color: #6b7280; 172 | font-size: 0.9rem; 173 | } 174 | 175 | @media (max-width: 768px) { 176 | header h1 { 177 | font-size: 2.2rem; 178 | } 179 | 180 | .features { 181 | grid-template-columns: 1fr; 182 | } 183 | } 184 | </style> 185 | </head> 186 | <body> 187 | <header> 188 | <div class="container"> 189 | <h1>Yourware MCP</h1> 190 | <p>An MCP server to upload your projects to <a href="https://www.yourware.so" style="color: white; text-decoration: underline;">yourware.so</a></p> 191 | </div> 192 | </header> 193 | 194 | <section id="overview"> 195 | <div class="container"> 196 | <h2>Project Overview</h2> 197 | <p> 198 | Yourware MCP is a Model Context Protocol (MCP) server that enables AI assistants like Claude to upload your projects directly to 199 | <a href="https://www.yourware.so">yourware.so</a>. This integration allows for seamless deployment of web projects, 200 | making it easy to showcase your work online without leaving your development environment. 201 | </p> 202 | <p> 203 | Whether you're building a simple HTML page, a complex web application, or any other web-based project, 204 | Yourware MCP streamlines the process of getting your work online and shareable. 205 | </p> 206 | </div> 207 | </section> 208 | 209 | <section id="features"> 210 | <div class="container"> 211 | <h2>Features & Capabilities</h2> 212 | <div class="features"> 213 | <div class="feature-card"> 214 | <h3>Easy Authentication</h3> 215 | <p>Simple API key-based authentication system that securely stores your credentials.</p> 216 | </div> 217 | <div class="feature-card"> 218 | <h3>Flexible Uploads</h3> 219 | <p>Upload single files or entire directories with automatic compression and organization.</p> 220 | </div> 221 | <div class="feature-card"> 222 | <h3>Seamless Deployment</h3> 223 | <p>One-step deployment process that makes your projects instantly available online.</p> 224 | </div> 225 | <div class="feature-card"> 226 | <h3>AI Assistant Integration</h3> 227 | <p>Works with AI assistants like Claude through Cline, Cursor, and Windsurf for a smooth workflow.</p> 228 | </div> 229 | </div> 230 | </div> 231 | </section> 232 | 233 | <section id="installation"> 234 | <div class="container"> 235 | <h2>Installation & Setup</h2> 236 | <p>Getting started with Yourware MCP is straightforward:</p> 237 | 238 | <ol class="installation-steps"> 239 | <li> 240 | <strong>Install the package</strong> 241 | <pre>pip install yourware-mcp</pre> 242 | <p>Or use <code>uvx</code> directly as shown in the configuration examples below.</p> 243 | </li> 244 | <li> 245 | <strong>Configure your AI assistant</strong> 246 | <p>Add the Yourware MCP server to your AI assistant's configuration:</p> 247 | <h3>General Configuration</h3> 248 | <pre>{ 249 | "mcpServers": { 250 | "yourware-mcp": { 251 | "command": "uvx", 252 | "args": ["yourware-mcp@latest", "stdio"], 253 | "env": {} 254 | } 255 | } 256 | }</pre> 257 | <h3>Cursor Configuration</h3> 258 | <p>In Cursor settings → features → MCP Servers, add a new MCP Server named <code>yourware-mcp</code> and set the command to <code>uvx yourware-mcp@latest stdio</code></p> 259 | <img src="../assets/config-cursor.png" alt="Config cursor screenshot" style="max-width: 100%; border-radius: 8px; margin-top: 10px;"> 260 | </li> 261 | <li> 262 | <strong>Create an API key</strong> 263 | <p>You can either:</p> 264 | <ul> 265 | <li>Let your AI assistant guide you through creating an API key</li> 266 | <li>Visit <a href="https://www.yourware.so/api/v1/api-keys/quick-create">https://www.yourware.so/api/v1/api-keys/quick-create</a> to create one manually</li> 267 | <li>Set the <code>YOURWARE_API_KEY</code> environment variable</li> 268 | </ul> 269 | </li> 270 | </ol> 271 | </div> 272 | </section> 273 | 274 | <section id="usage"> 275 | <div class="container"> 276 | <h2>Usage Examples</h2> 277 | <p>Once configured, you can use Yourware MCP through your AI assistant with the following tools:</p> 278 | 279 | <h3>Check Credentials</h3> 280 | <pre> 281 | // Check if your credentials exist and are valid 282 | await use_mcp_tool({ 283 | server_name: "yourware-mcp", 284 | tool_name: "check_credentials", 285 | arguments: {} 286 | });</pre> 287 | 288 | <h3>Create API Key</h3> 289 | <pre> 290 | // Create a new API key 291 | await use_mcp_tool({ 292 | server_name: "yourware-mcp", 293 | tool_name: "create_api_key", 294 | arguments: { 295 | api_key: "your-api-key-here" // Optional, if not provided, will guide you to create one 296 | } 297 | });</pre> 298 | 299 | <h3>Upload Project</h3> 300 | <pre> 301 | // Upload a file or directory to yourware 302 | await use_mcp_tool({ 303 | server_name: "yourware-mcp", 304 | tool_name: "upload_project", 305 | arguments: { 306 | file_path: "path/to/your/project" // Can be a directory or single file 307 | } 308 | });</pre> 309 | 310 | <h3>Example Workflow</h3> 311 | <p>A typical workflow might look like this:</p> 312 | <ol> 313 | <li>Build your web project</li> 314 | <li>Check if your credentials are valid</li> 315 | <li>If not, create a new API key</li> 316 | <li>Upload your project to Yourware</li> 317 | <li>Get the project URL and share it with others</li> 318 | </ol> 319 | </div> 320 | </section> 321 | 322 | <section id="resources"> 323 | <div class="container"> 324 | <h2>Resources & Links</h2> 325 | <div class="resources"> 326 | <div class="resource-card"> 327 | <h3>GitHub Repository</h3> 328 | <p>View the source code and contribute to the project.</p> 329 | <a href="https://github.com/ai-zerolab/yourware-mcp" class="btn">Visit GitHub</a> 330 | </div> 331 | <div class="resource-card"> 332 | <h3>Yourware Platform</h3> 333 | <p>Explore the Yourware platform and see examples.</p> 334 | <a href="https://www.yourware.so" class="btn">Visit Yourware</a> 335 | </div> 336 | <div class="resource-card"> 337 | <h3>Documentation</h3> 338 | <p>Read the full documentation for detailed information.</p> 339 | <a href="https://www.yourware.so/docs" class="btn">Read Docs</a> 340 | </div> 341 | </div> 342 | </div> 343 | </section> 344 | 345 | <footer> 346 | <div class="container"> 347 | <p>© 2025 Yourware MCP. All rights reserved.</p> 348 | </div> 349 | </footer> 350 | </body> 351 | </html> 352 | ```