iflow-mcp_modal-mcp-toolbox 0.1.12__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.
- iflow_mcp_modal_mcp_toolbox-0.1.12/.github/workflows/publish.yml +28 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/.gitignore +10 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/.python-version +1 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/Dockerfile +32 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/LICENSE +21 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/PKG-INFO +89 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/README.md +78 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/assets/claude-settings.png +0 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/assets/flux.gif +0 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/assets/goose-settings-1.png +0 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/assets/goose-settings-2.png +0 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/assets/python-sandbox.gif +0 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/pyproject.toml +41 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/smithery.yaml +21 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/src/modal_mcp_toolbox/__init__.py +29 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/src/modal_mcp_toolbox/__main__.py +3 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/src/modal_mcp_toolbox/code.py +75 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/src/modal_mcp_toolbox/flux.py +168 -0
- iflow_mcp_modal_mcp_toolbox-0.1.12/uv.lock +924 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
jobs:
|
|
4
|
+
publish-package:
|
|
5
|
+
name: Publish Package
|
|
6
|
+
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v5
|
|
16
|
+
|
|
17
|
+
- name: UV Sync
|
|
18
|
+
run: uv sync --all-extras --dev
|
|
19
|
+
|
|
20
|
+
- name: Build
|
|
21
|
+
run: uv build
|
|
22
|
+
|
|
23
|
+
- name: Publish
|
|
24
|
+
run: uv publish
|
|
25
|
+
|
|
26
|
+
on:
|
|
27
|
+
release:
|
|
28
|
+
types: [published]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
|
|
2
|
+
# Use a Python image with uv pre-installed
|
|
3
|
+
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv
|
|
4
|
+
|
|
5
|
+
# Install the project into /app
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
# Enable bytecode compilation
|
|
9
|
+
ENV UV_COMPILE_BYTECODE=1
|
|
10
|
+
|
|
11
|
+
# Copy from the cache instead of linking since it's a mounted volume
|
|
12
|
+
ENV UV_LINK_MODE=copy
|
|
13
|
+
|
|
14
|
+
# Install the project's dependencies using the lockfile and settings
|
|
15
|
+
RUN --mount=type=cache,target=/root/.cache/uv --mount=type=bind,source=uv.lock,target=uv.lock --mount=type=bind,source=pyproject.toml,target=pyproject.toml uv sync --frozen --no-install-project --no-dev --no-editable
|
|
16
|
+
|
|
17
|
+
# Then, add the rest of the project source code and install it
|
|
18
|
+
# Installing separately from its dependencies allows optimal layer caching
|
|
19
|
+
ADD . /app
|
|
20
|
+
RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev --no-editable
|
|
21
|
+
|
|
22
|
+
FROM python:3.12-slim-bookworm
|
|
23
|
+
|
|
24
|
+
WORKDIR /app
|
|
25
|
+
|
|
26
|
+
COPY --from=uv --chown=app:app /app/.venv /app/.venv
|
|
27
|
+
|
|
28
|
+
# Place executables in the environment at the front of the path
|
|
29
|
+
ENV PATH="/app/.venv/bin:$PATH"
|
|
30
|
+
|
|
31
|
+
# Set the entrypoint to execute the local project
|
|
32
|
+
ENTRYPOINT ["modal-mcp-toolbox"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Philipp Eisen
|
|
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.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iflow-mcp_modal-mcp-toolbox
|
|
3
|
+
Version: 0.1.12
|
|
4
|
+
Summary: A collection of Model Context Protocol (MCP) tools for Modal
|
|
5
|
+
Author-email: Philipp Eisen <hello@philippeisen.de>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: <3.13,>=3.10
|
|
8
|
+
Requires-Dist: mcp>=1.3.0
|
|
9
|
+
Requires-Dist: modal>=0.73.43
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Modal MCP Toolbox 🛠️
|
|
13
|
+
|
|
14
|
+
[](https://smithery.ai/server/@philipp-eisen/modal-mcp-toolbox)
|
|
15
|
+
|
|
16
|
+
A collection of Model Context Protocol (MCP) tools that run on Modal.
|
|
17
|
+
This let's you extend the capabilities of your LLM in tools such as [Goose](https://block.github.io/goose/) or the [Claude Desktop App](https://claude.ai/download).
|
|
18
|
+
|
|
19
|
+
<a href="https://glama.ai/mcp/servers/ai78w0p5mc"><img width="380" height="200" src="https://glama.ai/mcp/servers/ai78w0p5mc/badge" alt="Modal Toolbox MCP server" /></a>
|
|
20
|
+
|
|
21
|
+
## Tools
|
|
22
|
+
|
|
23
|
+
- `run_python_code_in_sandbox`: Let's you run python code in a sandboxed environment.
|
|
24
|
+
- `generate_flux_image`: Generate an image using the FLUX model.
|
|
25
|
+
|
|
26
|
+
## Demo
|
|
27
|
+
|
|
28
|
+
### Flux Image Generation
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
### Python Code Execution
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
## Prerequisites
|
|
37
|
+
|
|
38
|
+
- A [modal account](https://modal.com/signup) and a configured modal CLI.
|
|
39
|
+
- [UV](https://github.com/astral-sh/uv?tab=readme-ov-file#installation)
|
|
40
|
+
- A client that supports MCP. Such as the [Claude Desktop App](https://claude.ai/download) or [Goose](https://block.github.io/goose/)
|
|
41
|
+
|
|
42
|
+
This runs against your modal account, so you will need to have a modal account and be logged in.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
Installation depends on the client that uses the MCP. Here is instructions for Claude and Goose.
|
|
47
|
+
|
|
48
|
+
### Claude
|
|
49
|
+
|
|
50
|
+
Got to `Settings > Developer` in the Claude Desktop App. And click on Edit Config.
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
Add the config for the mcp server. My config looks like this:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"modal-toolbox": {
|
|
59
|
+
"command": "uvx",
|
|
60
|
+
"args": ["modal-mcp-toolbox"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Goose
|
|
67
|
+
|
|
68
|
+
Go to `Settings` and Click on Add.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
Then add an extension like in the screenshot below.
|
|
73
|
+
The important part is to set command to:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
uvx modal-mcp-toolbox
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The rest you can fill in as you like.
|
|
80
|
+
|
|
81
|
+

|
|
82
|
+
|
|
83
|
+
### Installing via Smithery (not working currently)
|
|
84
|
+
|
|
85
|
+
To install Modal MCP Toolbox for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@philipp-eisen/modal-mcp-toolbox):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx -y @smithery/cli install @philipp-eisen/modal-mcp-toolbox --client claude
|
|
89
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Modal MCP Toolbox 🛠️
|
|
2
|
+
|
|
3
|
+
[](https://smithery.ai/server/@philipp-eisen/modal-mcp-toolbox)
|
|
4
|
+
|
|
5
|
+
A collection of Model Context Protocol (MCP) tools that run on Modal.
|
|
6
|
+
This let's you extend the capabilities of your LLM in tools such as [Goose](https://block.github.io/goose/) or the [Claude Desktop App](https://claude.ai/download).
|
|
7
|
+
|
|
8
|
+
<a href="https://glama.ai/mcp/servers/ai78w0p5mc"><img width="380" height="200" src="https://glama.ai/mcp/servers/ai78w0p5mc/badge" alt="Modal Toolbox MCP server" /></a>
|
|
9
|
+
|
|
10
|
+
## Tools
|
|
11
|
+
|
|
12
|
+
- `run_python_code_in_sandbox`: Let's you run python code in a sandboxed environment.
|
|
13
|
+
- `generate_flux_image`: Generate an image using the FLUX model.
|
|
14
|
+
|
|
15
|
+
## Demo
|
|
16
|
+
|
|
17
|
+
### Flux Image Generation
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
### Python Code Execution
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
- A [modal account](https://modal.com/signup) and a configured modal CLI.
|
|
28
|
+
- [UV](https://github.com/astral-sh/uv?tab=readme-ov-file#installation)
|
|
29
|
+
- A client that supports MCP. Such as the [Claude Desktop App](https://claude.ai/download) or [Goose](https://block.github.io/goose/)
|
|
30
|
+
|
|
31
|
+
This runs against your modal account, so you will need to have a modal account and be logged in.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Installation depends on the client that uses the MCP. Here is instructions for Claude and Goose.
|
|
36
|
+
|
|
37
|
+
### Claude
|
|
38
|
+
|
|
39
|
+
Got to `Settings > Developer` in the Claude Desktop App. And click on Edit Config.
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
Add the config for the mcp server. My config looks like this:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"modal-toolbox": {
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["modal-mcp-toolbox"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Goose
|
|
56
|
+
|
|
57
|
+
Go to `Settings` and Click on Add.
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
Then add an extension like in the screenshot below.
|
|
62
|
+
The important part is to set command to:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
uvx modal-mcp-toolbox
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The rest you can fill in as you like.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
### Installing via Smithery (not working currently)
|
|
73
|
+
|
|
74
|
+
To install Modal MCP Toolbox for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@philipp-eisen/modal-mcp-toolbox):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx -y @smithery/cli install @philipp-eisen/modal-mcp-toolbox --client claude
|
|
78
|
+
```
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "iflow-mcp_modal-mcp-toolbox"
|
|
3
|
+
version = "0.1.12"
|
|
4
|
+
description = "A collection of Model Context Protocol (MCP) tools for Modal"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10,<3.13"
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "Philipp Eisen", email = "hello@philippeisen.de"},
|
|
9
|
+
]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"mcp>=1.3.0",
|
|
12
|
+
"modal>=0.73.43",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[dependency-groups]
|
|
16
|
+
dev = [
|
|
17
|
+
"mcp[cli]>=1.3.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.ruff]
|
|
21
|
+
line-length = 150
|
|
22
|
+
target-version = "py310"
|
|
23
|
+
select = [
|
|
24
|
+
"E", # pycodestyle errors
|
|
25
|
+
"W", # pycodestyle warnings
|
|
26
|
+
"F", # pyflakes
|
|
27
|
+
"I", # isort
|
|
28
|
+
"B", # flake8-bugbear
|
|
29
|
+
]
|
|
30
|
+
ignore = []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
modal-mcp-toolbox = "modal_mcp_toolbox:main"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/modal_mcp_toolbox"]
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
|
|
2
|
+
|
|
3
|
+
startCommand:
|
|
4
|
+
type: stdio
|
|
5
|
+
configSchema:
|
|
6
|
+
# JSON Schema defining the configuration options for the MCP.
|
|
7
|
+
type: object
|
|
8
|
+
required:
|
|
9
|
+
- modalTokenID
|
|
10
|
+
- modalTokenSecret
|
|
11
|
+
properties:
|
|
12
|
+
modalTokenID:
|
|
13
|
+
type: string
|
|
14
|
+
description: The ID for the Modal token. You can create the token at https://modal.com/settings/tokens
|
|
15
|
+
modalTokenSecret:
|
|
16
|
+
type: string
|
|
17
|
+
description: The secret for the Modal token. You can create the token at https://modal.com/settings/tokens
|
|
18
|
+
commandFunction:
|
|
19
|
+
# A function that produces the CLI command to start the MCP on stdio.
|
|
20
|
+
|-
|
|
21
|
+
(config) => ({ command: 'modal-mcp-toolbox', args: [], "env": { MODAL_TOKEN_ID: config.modalTokenID, MODAL_TOKEN_SECRET: config.modalTokenSecret } })
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
|
|
7
|
+
from modal_mcp_toolbox.code import run_python_code_in_sandbox
|
|
8
|
+
from modal_mcp_toolbox.flux import generate_flux_image
|
|
9
|
+
|
|
10
|
+
server = FastMCP("modal-toolbox")
|
|
11
|
+
|
|
12
|
+
server.add_tool(run_python_code_in_sandbox)
|
|
13
|
+
server.add_tool(generate_flux_image)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
"""MCP Modal Sandbox: A sandbox for running python code in a safe environment."""
|
|
21
|
+
parser = argparse.ArgumentParser(description="A sandbox for running python code in a safe environment.")
|
|
22
|
+
parser.add_argument("--version", action="version", version=version("iflow-mcp_modal-mcp-toolbox"))
|
|
23
|
+
parser.parse_args()
|
|
24
|
+
server.run()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()])
|
|
29
|
+
main()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import modal
|
|
5
|
+
from mcp import ErrorData, McpError
|
|
6
|
+
from mcp.types import INVALID_PARAMS, Annotations, TextContent
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def run_python_code_in_sandbox(
|
|
11
|
+
code: Annotated[str, Field(description="The python code to run.")],
|
|
12
|
+
requirements: Annotated[list[str] | None, Field(description="The requirements to install.")] = None,
|
|
13
|
+
python_version: Annotated[str, Field(description="The python version to use. If not provided defaults to 3.13")] = "3.13",
|
|
14
|
+
mount_directory: Annotated[
|
|
15
|
+
str | None,
|
|
16
|
+
Field(
|
|
17
|
+
description="Allows you to make a local directory available at `/mounted-dir` for the code in `code`. Needs to be an absolute path. "
|
|
18
|
+
"Writes to this directory will NOT be reflected in the local directory."
|
|
19
|
+
),
|
|
20
|
+
] = None,
|
|
21
|
+
pull_files: Annotated[
|
|
22
|
+
list[tuple[str, str]] | None,
|
|
23
|
+
Field(
|
|
24
|
+
description="List of tuples (absolut_path_sandbox_file, absolute_path_local_file). "
|
|
25
|
+
"When provided downloads the file(s) from the sandbox to the local file(s)."
|
|
26
|
+
),
|
|
27
|
+
] = None,
|
|
28
|
+
) -> TextContent:
|
|
29
|
+
"""
|
|
30
|
+
Runs python code in a safe environment and returns the output.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
run_python_code_in_sandbox("print('Hello, world!')")
|
|
34
|
+
run_python_code_in_sandbox("import requests\nprint(requests.get('https://icanhazip.com').text)", requirements=["requests"])
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
app = modal.App.lookup("mcp-toolbox--code", create_if_missing=True)
|
|
38
|
+
image = modal.Image.debian_slim(python_version=python_version).pip_install(requirements or [])
|
|
39
|
+
|
|
40
|
+
mounts: list[modal.Mount] = []
|
|
41
|
+
if mount_directory:
|
|
42
|
+
mounts.append(modal.Mount.from_local_dir(mount_directory, remote_path="/mounted-dir"))
|
|
43
|
+
|
|
44
|
+
sb = modal.Sandbox.create(image=image, app=app, mounts=mounts)
|
|
45
|
+
try:
|
|
46
|
+
exc = sb.exec("python", "-c", code)
|
|
47
|
+
exc.wait()
|
|
48
|
+
if exc.returncode != 0:
|
|
49
|
+
stderr = exc.stderr.read()
|
|
50
|
+
raise McpError(
|
|
51
|
+
ErrorData(
|
|
52
|
+
code=INVALID_PARAMS,
|
|
53
|
+
message=f"Error running code:\n{stderr}",
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if pull_files:
|
|
58
|
+
for remote_file, local_file in pull_files:
|
|
59
|
+
if not Path(local_file).parent.exists():
|
|
60
|
+
Path(local_file).parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
if Path(local_file).exists():
|
|
63
|
+
raise McpError(
|
|
64
|
+
ErrorData(
|
|
65
|
+
code=INVALID_PARAMS,
|
|
66
|
+
message=f"File {local_file} already exists.",
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
with sb.open(remote_file, "rb") as f:
|
|
70
|
+
with open(local_file, "wb") as f2:
|
|
71
|
+
f2.write(f.read())
|
|
72
|
+
return TextContent(type="text", text=exc.stdout.read(), annotations=Annotations(audience=["user", "assistant"], priority=0.5))
|
|
73
|
+
|
|
74
|
+
finally:
|
|
75
|
+
sb.terminate()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# based on https://github.com/modal-labs/modal-examples/blob/main/06_gpu_and_ml/stable_diffusion/flux.py
|
|
2
|
+
import logging
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import modal
|
|
8
|
+
from mcp.server.fastmcp import Context, Image
|
|
9
|
+
from mcp.types import Annotations, ImageContent
|
|
10
|
+
from modal.exception import NotFoundError
|
|
11
|
+
from modal.runner import deploy_app
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
MINUTES = 60 # seconds
|
|
17
|
+
VARIANT = "schnell" # or "dev", but note [dev] requires you to accept terms and conditions on HF
|
|
18
|
+
NUM_INFERENCE_STEPS = 4 # use ~50 for [dev], smaller for [schnell]
|
|
19
|
+
IMAGE_FORMAT = "JPEG"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
cuda_version = "12.4.0" # should be no greater than host CUDA version
|
|
23
|
+
flavor = "devel" # includes full CUDA toolkit
|
|
24
|
+
operating_sys = "ubuntu22.04"
|
|
25
|
+
tag = f"{cuda_version}-{flavor}-{operating_sys}"
|
|
26
|
+
|
|
27
|
+
cuda_dev_image = modal.Image.from_registry(f"nvidia/cuda:{tag}", add_python="3.11").entrypoint([])
|
|
28
|
+
diffusers_commit_sha = "81cf3b2f155f1de322079af28f625349ee21ec6b"
|
|
29
|
+
|
|
30
|
+
flux_image = (
|
|
31
|
+
cuda_dev_image.apt_install(
|
|
32
|
+
"git",
|
|
33
|
+
"libglib2.0-0",
|
|
34
|
+
"libsm6",
|
|
35
|
+
"libxrender1",
|
|
36
|
+
"libxext6",
|
|
37
|
+
"ffmpeg",
|
|
38
|
+
"libgl1",
|
|
39
|
+
)
|
|
40
|
+
.pip_install(
|
|
41
|
+
"invisible_watermark==0.2.0",
|
|
42
|
+
"transformers==4.44.0",
|
|
43
|
+
"huggingface_hub[hf_transfer]==0.26.2",
|
|
44
|
+
"accelerate==0.33.0",
|
|
45
|
+
"safetensors==0.4.4",
|
|
46
|
+
"sentencepiece==0.2.0",
|
|
47
|
+
"torch==2.5.0",
|
|
48
|
+
f"git+https://github.com/huggingface/diffusers.git@{diffusers_commit_sha}",
|
|
49
|
+
"numpy<2",
|
|
50
|
+
# This is a bit of a hack to ensure that the the version modal-mcp-toolbox is the same as the local version.
|
|
51
|
+
# -- not really ideal
|
|
52
|
+
f"iflow-mcp_modal-mcp-toolbox=={version('iflow-mcp_modal-mcp-toolbox')}",
|
|
53
|
+
)
|
|
54
|
+
.env({"HF_HUB_ENABLE_HF_TRANSFER": "1", "HF_HUB_CACHE": "/cache"})
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
flux_image = flux_image.env(
|
|
59
|
+
{
|
|
60
|
+
"TORCHINDUCTOR_CACHE_DIR": "/root/.inductor-cache",
|
|
61
|
+
"TORCHINDUCTOR_FX_GRAPH_CACHE": "1",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
app_name = "mcp-toolbox--flux"
|
|
67
|
+
app = modal.App(app_name, image=flux_image)
|
|
68
|
+
|
|
69
|
+
with flux_image.imports():
|
|
70
|
+
import torch
|
|
71
|
+
from diffusers import FluxPipeline
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.cls(
|
|
75
|
+
gpu="L40S",
|
|
76
|
+
scaledown_window=5 * MINUTES,
|
|
77
|
+
image=flux_image,
|
|
78
|
+
volumes={
|
|
79
|
+
"/cache": modal.Volume.from_name("hf-hub-cache", create_if_missing=True),
|
|
80
|
+
},
|
|
81
|
+
enable_memory_snapshot=True,
|
|
82
|
+
)
|
|
83
|
+
class Model:
|
|
84
|
+
@modal.enter(snap=True)
|
|
85
|
+
def load(self):
|
|
86
|
+
print("🔄 loading model...")
|
|
87
|
+
pipe = FluxPipeline.from_pretrained(f"black-forest-labs/FLUX.1-{VARIANT}", torch_dtype=torch.bfloat16)
|
|
88
|
+
self.pipe = _optimize(pipe)
|
|
89
|
+
|
|
90
|
+
@modal.enter(snap=False)
|
|
91
|
+
def setup(self):
|
|
92
|
+
print("🔄 moving model to GPU...")
|
|
93
|
+
self.pipe = self.pipe.to("cuda")
|
|
94
|
+
|
|
95
|
+
@modal.method()
|
|
96
|
+
def inference(self, prompt: str) -> bytes:
|
|
97
|
+
print("🎨 generating image...")
|
|
98
|
+
out = self.pipe(
|
|
99
|
+
prompt,
|
|
100
|
+
output_type="pil",
|
|
101
|
+
num_inference_steps=NUM_INFERENCE_STEPS,
|
|
102
|
+
).images[0]
|
|
103
|
+
|
|
104
|
+
byte_stream = BytesIO()
|
|
105
|
+
out.save(byte_stream, format=IMAGE_FORMAT)
|
|
106
|
+
return byte_stream.getvalue()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.function(
|
|
110
|
+
# This is a bit of a hack to ensure that the the version modal-mcp-toolbox is the same as the local version.
|
|
111
|
+
# -- not really ideal
|
|
112
|
+
image=modal.Image.debian_slim().pip_install(f"iflow-mcp_modal-mcp-toolbox=={version('iflow-mcp_modal-mcp-toolbox')}"),
|
|
113
|
+
scaledown_window=5 * MINUTES,
|
|
114
|
+
)
|
|
115
|
+
def get_version():
|
|
116
|
+
try:
|
|
117
|
+
print("modal_mcp_toolbox version:", version("modal_mcp_toolbox"))
|
|
118
|
+
return version("modal_mcp_toolbox")
|
|
119
|
+
except PackageNotFoundError:
|
|
120
|
+
print("modal_mcp_toolbox version: unknown")
|
|
121
|
+
return "unknown"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _optimize(pipe):
|
|
125
|
+
# fuse QKV projections in Transformer and VAE
|
|
126
|
+
pipe.transformer.fuse_qkv_projections()
|
|
127
|
+
pipe.vae.fuse_qkv_projections()
|
|
128
|
+
|
|
129
|
+
# switch memory layout to Torch's preferred, channels_last
|
|
130
|
+
pipe.transformer.to(memory_format=torch.channels_last)
|
|
131
|
+
pipe.vae.to(memory_format=torch.channels_last)
|
|
132
|
+
|
|
133
|
+
return pipe
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def _ensure_app_deployment_is_up_to_date(ctx: Context):
|
|
137
|
+
try:
|
|
138
|
+
fn = modal.Function.from_name(app_name, "get_version")
|
|
139
|
+
remote_version = await fn.remote.aio()
|
|
140
|
+
|
|
141
|
+
if remote_version != version("iflow-mcp_modal-mcp-toolbox"):
|
|
142
|
+
await ctx.info("App is out of date. Deploying ...")
|
|
143
|
+
logger.info("App is out of date. Deploying ...")
|
|
144
|
+
deploy_app(app)
|
|
145
|
+
except NotFoundError:
|
|
146
|
+
await ctx.info("App not found. Deploying...")
|
|
147
|
+
logger.info("App not found. Deploying...")
|
|
148
|
+
deploy_app(app)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def generate_flux_image(prompt: Annotated[str, Field(description="The prompt to generate an image for")], ctx: Context) -> ImageContent:
|
|
152
|
+
"""Let's you generate an image using the Flux model."""
|
|
153
|
+
await _ensure_app_deployment_is_up_to_date(ctx)
|
|
154
|
+
|
|
155
|
+
cls = modal.Cls.from_name(app_name, Model.__name__)
|
|
156
|
+
image_bytes = await cls().inference.remote.aio(prompt)
|
|
157
|
+
image_content = Image(data=image_bytes, format=IMAGE_FORMAT).to_image_content()
|
|
158
|
+
image_content.annotations = Annotations(audience=["user", "assistant"], priority=0.5)
|
|
159
|
+
return image_content
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
deploy_app(app)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@app.local_entrypoint()
|
|
167
|
+
async def main():
|
|
168
|
+
print(await get_version.remote.aio())
|