#
tokens: 2523/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── model_app.py
├── pyproject.toml
├── README.md
├── src
│   └── modal_server
│       ├── __init__.py
│       └── server.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# mcp-server-modal

https://docs.google.com/document/d/1DcrSKbcsXrzCoyMe0XtAsDcE3IgBV1bLirUG80VxPq8/edit?tab=t.0

An MCP Server that allows users to deploy python scripts to [modal](https://modal.com/).

## Installation

Make sure that modal is setup:

```
pip install modal
python3 -m modal setup
```

Then setup the server with the filesystem server in your Claude desktop app:

```
{
   "mcpServers": {
        "mcp-server-modal": {
            "command": "uv",
            "args": [
                "--directory",
                "/path/to/mcp-server-modal",
                "run",
                "modal-server"
            ]
        },
        "filesystem": {
			"command": "npx",
			"args": [
				"-y",
				"@modelcontextprotocol/server-filesystem",
				"/Users/user/Desktop/",
                "/path/to/other/dir"
			]
		}
   }
}
```

## Usage

In claude, give a python script and ask it to create a modal application and deploy it for you. After code generation, you will get a link to the modal application which you can try out and share with others.

## Development

```
npx @modelcontextprotocol/inspector uv --directory /path/to/mcp-server-modal run modal-server
```

```

--------------------------------------------------------------------------------
/model_app.py:
--------------------------------------------------------------------------------

```python
print("deploy result from model_app.py")

```

--------------------------------------------------------------------------------
/src/modal_server/__init__.py:
--------------------------------------------------------------------------------

```python
from . import server
import asyncio


def main():
    """Main entry point for the package."""
    asyncio.run(server.main())


# Optionally expose other important items at package level
__all__ = ["main", "server"]

```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "modal-server"
version = "0.1.0"
description = ""
readme = "README.md"
requires-python = ">=3.12,<3.13"
dependencies = [
    "httpx>=0.28.1",
    "mcp>=1.1.1",
    "python-dotenv>=1.0.1",
    "modal>=0.67",
]
[[project.authors]]
name = "Server Modal"
email = "[email protected]"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project.scripts]
modal-server = "modal_server:main"

```

--------------------------------------------------------------------------------
/src/modal_server/server.py:
--------------------------------------------------------------------------------

```python
import json
import logging
from collections.abc import Sequence
import subprocess
from typing import Any

import httpx
import asyncio
from dotenv import load_dotenv
from mcp.server import Server
from mcp.types import (
    Resource,
    Tool,
    TextContent,
    ImageContent,
    EmbeddedResource,
)
import mcp.types as types
from mcp.server import Server
from pydantic import AnyUrl

import mcp.server.stdio

from pydantic import AnyUrl

# Load environment variables
load_dotenv()

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("modal-server")

notes: dict[str, str] = {}

app = Server("modal-server")


@app.list_resources()
async def list_resources() -> list[Resource]:
    """List available resources."""
    return []


@app.read_resource()
async def handle_read_resource(uri: AnyUrl) -> str:
    try:
        return json.dumps({"result": "example"}, indent=2)
    except httpx.HTTPError as e:
        raise RuntimeError(f"API error: {str(e)}")


@app.list_prompts()
async def handle_list_prompts() -> list[types.Prompt]:
    """
    List available prompts.
    Each prompt can have optional arguments to customize its behavior.
    """
    return [
        types.Prompt(
            name="summarize-notes",
            description="Creates a summary of all notes",
            arguments=[
                types.PromptArgument(
                    name="style",
                    description="Style of the summary (brief/detailed)",
                    required=False,
                )
            ],
        )
    ]


@app.get_prompt()
async def handle_get_prompt(
    name: str, arguments: dict[str, str] | None
) -> types.GetPromptResult:
    """
    Generate a prompt by combining arguments with server state.
    The prompt includes all current notes and can be customized via arguments.
    """
    if name != "summarize-notes":
        raise ValueError(f"Unknown prompt: {name}")

    style = (arguments or {}).get("style", "brief")
    detail_prompt = " Give extensive details." if style == "detailed" else ""

    return types.GetPromptResult(
        description="Summarize the current notes",
        messages=[
            types.PromptMessage(
                role="user",
                content=types.TextContent(
                    type="text",
                    text=f"Here are the current notes to summarize:{detail_prompt}\n\n"
                    + "\n".join(
                        f"- {name}: {content}" for name, content in notes.items()
                    ),
                ),
            )
        ],
    )


@app.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    """
    List available tools.
    Each tool specifies its arguments using JSON Schema validation.
    """
    return [
        Tool(
            name="deploy",
            description="some description",
            inputSchema={
                "type": "object",
                "properties": {
                    "modal_path": {"type": "string"},
                },
                "required": ["message"],
            },
        )
    ]


@app.call_tool()
async def call_tool(
    name: str, arguments: Any
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
    """Handle tool calls for weather forecasts."""
    if name != "deploy":
        raise ValueError(f"Unknown tool: {name}")
    if not isinstance(arguments, dict) or "modal_path" not in arguments:
        raise ValueError("Invalid forecast arguments")
    modal_path = arguments["modal_path"]

    try:
        res = deploy(modal_path)
        return [
            TextContent(type="text", text=json.dumps(f"Deploy result: {res}", indent=2))
        ]
    except httpx.HTTPError as e:
        raise RuntimeError(f"Ran in error: {str(e)}")


def deploy(modal_path: str = "model_app.py") -> str:
    """
    Deploy a model using Modal CLI command.

    Args:
        modal_path: Path to the modal file to deploy

    Returns:
        str: deployment result
    """
    try:
        # Run modal deploy command
        process = subprocess.run(["modal", "deploy", modal_path], capture_output=True, text=True)
        
        # Check if the command was successful
        if process.returncode == 0:
            return f"Deploy success: {process.stdout}"
        else:
            raise RuntimeError(f"Deploy failed: {process.stderr}")
        # if process.returncode == 0:
        #     message = f"Deployment successful: {stdout.decode()}"
        # else:
        #     message = f"Deployment failed: {stderr.decode()}"
        # return message
    except Exception as e:
        return f"Deployment error: {str(e)}"


async def main():
    from mcp.server.stdio import stdio_server

    async with stdio_server() as (read_stream, write_stream):
        await app.run(read_stream, write_stream, app.create_initialization_options())

```