Build MCP Server Python FastMCP with uvx Packaging
Build an MCP server in Python with FastMCP by creating a FastMCP instance and registering typed Python functions with @mcp.tool(). FastMCP can derive tool schemas from annotations, validate incoming arguments, and handle MCP protocol details; uv packages the project so clients can launch it with uvx. This tutorial builds a runnable server, adds context logging, tests the package boundary, and configures a stdio client.
What you will build
The example is a small weather-style server. It returns placeholder data rather than calling a real weather API, so it needs no credentials while still showing the implementation details that matter in a real integration:
- A
FastMCPserver instance - A decorator-registered tool
- Typed and constrained tool arguments
- A correctly injected
Contextparameter - Client-visible logging without corrupting stdout
- A
pyproject.tomlconsole script that runs throughuvx
FastMCP is a Python framework for MCP applications. It is not safe to assume that every FastMCP release has identical APIs, especially around testing helpers, CLI commands, and context injection. The code below uses the standalone fastmcp package and should be tested against the version recorded in your lockfile.
For protocol concepts and design decisions outside the FastMCP implementation, see the MCP server development guide. This article concentrates on a practical FastMCP project and its packaging workflow.
Create the project with uv
Install uv using the method appropriate for your operating system, then create a project:
The uv add command adds the dependency to pyproject.toml and updates uv.lock. Commit the lockfile so local development and CI resolve the same dependency versions. For a server distributed to other people, pin FastMCP and other compatibility-sensitive dependencies after you have tested them.
Check the installed package from the project environment:
The version attribute is not guaranteed to exist in every package release, which is why the command uses getattr. The important check is that the import succeeds inside the same environment used by uv run and later by the built package.
Create this layout:
A src layout makes accidental imports from the repository root less likely. It also forces you to verify that the package contains the module that the eventual wheel and console script need.
Define the FastMCP server
Put the following code in src/weather_mcp/server.py:
The parameter order is important Python syntax as well as an integration detail. ctx comes before units, because a required parameter cannot follow a parameter with a default value. The same rule applies to every defaulted argument in a tool signature.
The @mcp.tool() decorator registers get_weather as an MCP tool. The function name is normally used as the tool name, and the docstring can provide tool documentation. Keep names stable after clients, prompts, or automation depend on them. If your FastMCP version supports explicit decorator options for names or descriptions, use those deliberately and verify the resulting schema.
city: str describes a string input. Literal["celsius", "fahrenheit"] communicates an enum-like constraint to schema generation and runtime validation. The default makes units optional for callers. The return annotation documents the intended result shape, although exact conversion behavior can vary between FastMCP versions.
The Context parameter is framework-injected rather than an ordinary tool argument. Declaring it as ctx: Context is the least ambiguous form for the versions that use signature inspection to identify context parameters. Avoid changing it casually to Context | None or Optional[Context]; some releases or configurations may then interpret it as a normal input or fail during registration.
The private helper is intentionally independent of MCP. That makes ordinary unit tests simple:
For an integration test, exercise the decorated server through a FastMCP-supported client or test transport for the exact version in uv.lock. Do not call get_weather("Paris", ...) as if it were an ordinary function unless you also provide the framework context expected by that release.
Use typed arguments as a contract
Automatic schemas are useful only when the annotations represent the real contract. A search tool might look like this:
Again, ctx precedes the defaulted limit and order parameters. The annotations help the client understand what to send, but they do not enforce operational policy by themselves. An integer can be syntactically valid and still be too large for your database or API. Validate lengths, ranges, identifiers, file paths, and permissions before performing expensive or privileged work.
For nested input, use an explicit typed model if the FastMCP release and model library in your project support it. A small public input model is usually safer than accepting an arbitrary dictionary. It produces a clearer contract and reduces the amount of unvalidated data reaching the implementation.
Log through Context without breaking stdio
A stdio MCP server uses stdout for protocol traffic. A stray print() can insert non-protocol text into that stream and cause the client to report a JSON or transport error. Use context methods for messages intended for the connected client:
Context logging is suitable for progress and user-visible diagnostics. Whether a client displays info, debug, or other notifications varies by client and version, so test with the clients you intend to support.
For operational logs that should remain on the server, use Python's logging module and configure a handler that writes to stderr, a file, or your process supervisor. Do not log API keys, access tokens, authorization headers, or sensitive tool arguments. Context messages may be shown to users or retained by a client.
Run the server locally
Install the locked environment and run the module:
The process will wait for MCP messages on stdin when using the default stdio behavior. Do not expect a browser page or ordinary terminal output. If you need a development CLI, inspect the commands provided by your installed release:
CLI options and transport defaults can vary, so verify the version-specific behavior before using a development command in production configuration. For local desktop integrations, stdio is often the simplest transport because the client starts and supervises the server process.
You can also use the AllMCPs MCP playground for interactive inspection where the server type and connection options are supported.
Package the server for uvx
Configure package metadata and a console script in pyproject.toml:
uv init may generate some of these fields already. Add the project.scripts table instead of duplicating an existing section. The left side, weather-mcp, is the executable command. The right side points to the importable module and function that starts the server.
Run the entry point locally:
Now build a wheel and test that artifact in an isolated environment:
The generated filename changes when the version changes, so list the dist directory if necessary. Testing the wheel matters: uv run from the repository can succeed even when package discovery, dependencies, or console-script metadata are incomplete.
After publishing the distribution to a package index, the same command becomes:
The distribution name and executable name do not have to match, but matching them reduces configuration errors. For a Git-based package, use a tag or commit when reproducibility matters rather than tracking a moving branch.
Configure an MCP client
A typical stdio configuration launches the package through uvx:
File locations and configuration keys differ between clients. Use the relevant guides for Claude Desktop, Cursor, VS Code with GitHub Copilot, Cline, or Claude Code. Some clients support an env object; others require environment variables to be configured in the operating system or shell.
For a server that needs credentials, pass environment variables instead of command-line arguments where the client supports it:
Do not commit real secrets. Confirm how your chosen client stores configuration and whether it passes the env mapping to child processes.
Verify and troubleshoot the package
Run the exact launch command outside the client first:
Then work through these checks:
- The command is not found. Confirm that the package contains the
project.scriptsentry and that the argument after--fromis the executable name. - The module cannot be imported. Test the built wheel rather than only running from the repository. Confirm that
src/weather_mcp/__init__.pyandserver.pyare included. - The client reports malformed protocol data. Remove
print()calls and route ordinary logs to stderr. Stdout must remain available for MCP traffic. ctxappears in the tool schema. Check that the parameter is annotated as the requiredContexttype and that it appears before all defaulted parameters. Confirm that the installed FastMCP version matches your tested version.- The schema is too permissive. Replace untyped dictionaries and strings with concrete annotations,
Literalvalues, or supported typed models, then add application-level validation. - A dependency works locally but not with
uvx. Add it to[project].dependencies; packages installed manually in your development environment are not automatically included in the isolateduvxenvironment. - Relative paths fail in the client. Resolve paths deliberately or use package resources and environment variables. A client may launch the process with a different working directory.
Once the server works, pin the FastMCP version and commit uv.lock. A lockfile gives reproducible project resolution, while an exact requirement in pyproject.toml communicates a stricter compatibility expectation to package installers. Re-run the wheel test after dependency upgrades because generated schemas, context injection, and CLI behavior can change between releases.
Next steps
Start with one typed tool and verify both uv run and the built-wheel uvx command before adding external APIs or privileged operations. Review the FastMCP directory listing, then connect the package using the MCP installation guide. For transport, security, and architecture decisions beyond this example, return to the MCP server development guide.
Frequently asked questions
Is FastMCP part of the Python MCP SDK?
FastMCP and the official Python MCP SDK are related but should not be treated as interchangeable package APIs. FastMCP is commonly used as a Python framework with decorator-based server registration, while the official SDK is distributed separately and its APIs depend on the release you install. This tutorial uses the standalone fastmcp package and recommends pinning the version you test.
How do I run a FastMCP server with uvx?
Define a console script in pyproject.toml, build or publish the package, and run that command with uvx --from. For example, uvx --from weather-mcp weather-mcp installs the distribution into an isolated environment and starts its executable. Test the command against a built wheel before publishing so missing modules and dependencies are found early.
How does FastMCP create schemas from Python types?
FastMCP inspects a decorated function's signature and annotations to generate the tool input schema exposed through MCP. Types such as str, int, lists, dictionaries, defaults, and Literal values can describe the expected inputs. Generated schemas do not replace application checks, so enforce limits, authorization, path restrictions, and other business rules inside the tool.
How should a FastMCP server write logs?
Use FastMCP Context methods such as await ctx.info() when a diagnostic or progress message should be sent to the connected MCP client. Never use ordinary print calls for debugging in a stdio server because stdout carries protocol messages. Configure Python logging for server-side diagnostics and send those records to stderr or a managed log sink.