Skip to main content

TST, Hongkong

How to deploy a build MCP server

Python Package Deployment​

Initialization the Server Package​

I am just building a simple MCP server that allows an agent to use the bash terminal for the sake of this tutorial:

mcp-servers/mcp-server-deployment master ❯ uv init
Initialized project `mcp-server-deployment`
source .venv/bin/activate
uv add fastmcp

Start by editing the project file to your liking:

nano pyproject.toml
[project]
name = "agent_terminal_tools"
version = "0.1.0"
description = "A MCP server that adds terminal tools to your agent."
readme = "README.md"
authors = [
{ name = "Mike Polinowski", email = "mpolinowski@gmail.com" }
]
requires-python = ">=3.14"
dependencies = [
"fastmcp>=4.0.11",
]

[project.scripts]
agent_terminal_tools = "agent_terminal_tools.main:main" # link agent_terminal_tools to execute your main()

[build-system]
requires = ["uv_build>=0.12.6,<0.13.0"]
build-backend = "uv_build" # use uv build command to build the package

[tool.uv_build.packages.find]
where = ["src"] # where is the src code to build from - defaults to ./src, just being verbose here

Changing the project name also forced me to re-name the auto generated folder src/mcp-server-deployment to src/agent_terminal_tools respectively.

MCP Server​

Ok - now we need to create the MCP tools that will be used by the server. Since the PyProject file above already defines it as such we will have to place this file in src/agent_terminal_tools/tools.py and then import the created MCP server instance into src/agent_terminal_tools/main.py. Let's start with the first one:

mkdir src/agent_terminal_tools/
nano src/agent_terminal_tools/tools.py

This is only an excerpt of the tools file - check out the Github repo for the complete list:

from fastmcp import FastMCP

mcp = FastMCP(
name="agent-terminal-tools",
instructions=(
"Tools for running common shell commands and file operations: "
"execute a bash command, run files and Python scripts, list "
"directories, glob and grep files, and read, write, append, delete, "
"copy, move, create and inspect files and folders. Every tool returns "
"a single string containing either the result or a clear error "
"message."
),
)


# ---------------------------------------------------------------------------
# Internal helper
# ---------------------------------------------------------------------------

def _run_command(command, use_shell: bool = False) -> str:
"""Run a command and return one decoded string describing the outcome.

``command`` is either a single string (``use_shell=True``) or a list of
argv tokens (``use_shell=False``). Both stdout and stderr are captured so a
failing command can always be diagnosed. On success this returns the
command's stdout (with a trailing stderr block included if any was written);
on failure it returns the exit code together with stderr.
"""
label = command if use_shell else " ".join(map(str, command))
try:
result = subprocess.run(
command,
shell=use_shell,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except subprocess.CalledProcessError as err:
stderr = (err.stderr or b"").decode("utf-8", errors="replace").strip()
body = f"Command failed with exit code {err.returncode}: {label}"
if stderr:
body += f"\n{stderr}"
return body
except (FileNotFoundError, PermissionError) as err:
return f"Could not run the command ({label}): {err}"

stdout = (result.stdout or b"").decode("utf-8", errors="replace").strip()
stderr = (result.stderr or b"").decode("utf-8", errors="replace").strip()
if stdout and stderr:
return f"{stdout}\n[stderr]\n{stderr}"
if stdout:
return stdout
if stderr:
return stderr
return "Command completed successfully (no output)."


# ---------------------------------------------------------------------------
# Shell execution
# ---------------------------------------------------------------------------

@mcp.tool()
def bash(command: str):
'''This function will execute a bash command and return the output. This function is useful for executing bash commands including creating files and directories'''
return _run_command(command, use_shell=True)

# Only one tool shown here - check Github for complete list.

And now the main file - i will add 2 different imports on top so that I can also start the main.py file directly for testing. The first import path is automatically being used when the server is packaged up :

nano src/agent_terminal_tools/main.py
"""Entry point: run the terminal-tools MCP server over stdio.

Runs the FastMCP server (and all of its tools) defined in ``tools.py``.
``main()`` is what the ``mcp-server-deployment`` console script calls; the
``if __name__ == "__main__"`` block also lets you start it directly with
``python main.py`` from within the package directory.
"""

try: # Package mode: imported as ``agent_terminal_tools.main``.
from agent_terminal_tools.tools import mcp
except ImportError: # Script mode: launched as ``python main.py`` from the package dir.
from tools import mcp


def main() -> None:
"""Start the MCP server over stdio."""
mcp.run(transport="stdio")


if __name__ == "__main__":
main()

I can test that the server is working by directly executing the main.py script:

…/src/agent_terminal_tools master ? ❯ python main.py


╭──────────────────────────────────────────────────────────────────────────────╮
│ ▄▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█ │
│ █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀ │
│ FastMCP 4.0.11 │
│ https://gofastmcp.com │
│ 🖥 Server: agent-terminal-tools, 4.0.11 │
│ 🚀 Deploy free: https://horizon.prefect.io │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
[10/09/26 08:33:47] INFO Starting MCP server 'agent-terminal-tools' with transport 'stdio'

Building the Package​

We can build the server package by running uv build from the app root as defined in PyProject:

mcp-servers/mcp-server-deployment master ? ❯ uv build
Building source distribution...
Building wheel from source distribution...
Successfully built dist/agent_terminal_tools-0.1.0.tar.gz
Successfully built dist/agent_terminal_tools-0.1.0-py3-none-any.whl

mcp-servers/mcp-server-deployment master ? ❯ ls -lah dist/
Permissions Size User Date Modified Name
.rw-r--r-- 1 user 9 Oct 11:46 󰊢 .gitignore
.rw-r--r-- 5.8k user 9 Oct 11:46  agent_terminal_tools-0.1.0-py3-none-any.whl
.rw-r--r-- 4.3k user 9 Oct 11:46  agent_terminal_tools-0.1.0.tar.gz

The Python Wheel can now be installed directly from the root directory into the virtual environment:

mcp-servers/mcp-server-deployment master ? ❯ uv pip install dist/agent_terminal_tools-0.1.0-py3-none-any.whl
Resolved 69 packages in 2.10s
Built agent-terminal-tools @ file:///home/user/Projects/mcp-servers/mcp-server-deployment Prepared 1 package in 7ms
Uninstalled 1 package in 0.48ms
Installed 1 package in 1ms
- agent-terminal-tools==0.1.0 (from file:///home/user/Projects/mcp-servers/mcp-server-deployment/dist/agent_terminal_tools-0.1.0-py3-none-any.whl)
+ agent-terminal-tools==0.1.0 (from file:///home/user/Projects/mcp-servers/mcp-server-deployment)

And it is installed:

mcp-servers/mcp-server-deployment master ? ❯ ls -lah .venv/bin/agent_terminal_tools
Permissions Size User Date Modified Name
.rwxr-xr-x 365 user 9 Oct 12:00 󰡯 .venv/bin/agent_terminal_tools
mcp-servers/mcp-server-deployment master ? ✗ uv pip list
Package Version Editable project location
------------------------- -------- --------------------------------------------------------
agent-terminal-tools 0.1.0 /home/user/Projects/mcp-servers/mcp-server-deployment

This means we can now test the MCP server by importing our new package. Since our main() starts the server in STDIO mode let's just run the server in HTTP mode manually now to see if this is also working:

`mcp-servers/mcp-server-deployment master ? ❯ `nano test_package.py
from agent_terminal_tools.tools import mcp

mcp.run(transport="streamable-http")
mcp-servers/mcp-server-deployment master ? ❯ python ./test_package.py

╭──────────────────────────────────────────────────────────────────────────────╮
│ ▄▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█ │
│ █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀ │
│ FastMCP 4.0.11 │
│ https://gofastmcp.com │
│ 🖥 Server: agent-terminal-tools, 4.0.11 │
│ 🚀 Deploy free: https://horizon.prefect.io │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯

[10/09/26 12:34:06] INFO Starting MCP server 'agent-terminal-tools' with transport 'streamable-http' transport.py:363
on http://127.0.0.1:8000/mcp
INFO: Started server process [590343]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

But we also registered an CLI command inside our PyProject:

[project.scripts]
agent_terminal_tools = "agent_terminal_tools.main:main"

So let's see if this is also correctly resolved inside our virtual environment. This will use the included main.py from our package and start the server in STDIO mode:

mcp-servers/mcp-server-deployment master ? ❯ agent_terminal_tools


╭──────────────────────────────────────────────────────────────────────────────╮
│ ▄▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█ │
│ █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀ │
│ FastMCP 4.0.11 │
│ https://gofastmcp.com │
│ 🖥 Server: agent-terminal-tools, 4.0.11 │
│ 🚀 Deploy free: https://horizon.prefect.io │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯

[10/09/26 12:26:07] INFO Starting MCP server 'agent-terminal-tools' with transport 'stdio'

Using the Package​

You can now push your package to PyPI or a local registry and have others install it from there. In this case the installation will be identical to the community server. But let's just write a template client that executes our locally installed package and that you can add as documentation on how to use the server:

To make this happen I first need the LanChain MCP Adapters inside my virtual environment:

mcp-servers/mcp-server-deployment master ? ❯ uv add langchain_mcp_adapters

But then there was the Installation Headache that required me to start the client from a different environment. So in my case I need to install the adapters in .venv-client and switch my environment from there while starting the server from the original .venv ~ this issue might be resolved by the time you are reading this, though. So I will setup the client environment and switch to it before continuing:

mcp-servers/mcp-server-deployment master ? ❯ source .venv-client/bin/activate

And now a simple LangChain client that lists all available tools for us:

mcp-servers/mcp-server-deployment master ? ❯ nano ./test_client.py
import os
import asyncio

from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.sessions import Connection
from langchain_mcp_adapters.tools import load_mcp_tools

venv_path = os.path.join(os.path.dirname(os.path.abspath(__name__)), ".venv")

# MCP Server config
connections: dict[str, Connection] = {
"agent_terminal_tools": {
"transport": "stdio",
"command": os.path.join(venv_path, "bin", "python"),
"args": [os.path.join(venv_path, "bin", "agent_terminal_tools")],
}
}

async def main():
client = MultiServerMCPClient(connections)

async with client.session("agent_terminal_tools") as session:
# list all the tools
tools = await load_mcp_tools(session)
for tool in tools:
print(f"Tool: {tool.name}")

if __name__ == "__main__":
asyncio.run(main())
mcp-servers/mcp-server-deployment master ? ❯ python ./test_client.py


╭──────────────────────────────────────────────────────────────────────────────╮
│ ▄▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█ │
│ █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀ │
│ FastMCP 4.0.11 │
│ https://gofastmcp.com │
│ 🖥 Server: agent-terminal-tools, 4.0.11 │
│ 🚀 Deploy free: https://horizon.prefect.io │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯


[10/09/26 13:47:21] INFO Starting MCP server 'agent-terminal-tools' with transport 'stdio'

Tool: bash
Tool: execute_file
Tool: run_python
Tool: list_directory
Tool: glob_files
Tool: grep
Tool: read_file
Tool: write_file
Tool: append_file
Tool: delete_file
Tool: create_folder
Tool: delete_folder
Tool: copy_file
Tool: move_file
Tool: file_info

Docker Deployment​

mcp-servers/mcp-docker-deployment master ? ❯ tree
.
├── app
│   └── mcp_gateway.py
├── Dockerfile
└── requirements.txt
  • app/mcp_gateway.py: -> Any of your MCP servers ~ just make sure you add all dependencies to:

  • requirements.txt:

fastmcp
  • Dockerfile:
FROM python:3.14-slim

# install uv / uvx
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app

# pre-install all mcp servers you want to use and are not part of your source code
RUN uv tool install duckduckgo-mcp-server

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# ensure uv is available
ENV PATH="/root/.local/bin:${PATH}"

CMD ["python", "app/mcp_gateway.py"]
docker build -t mcp_gateway .
docker run -p 8085:8050`--name mcp_gateway mcp_gateway