FastMCP Is Now MCPServer on AWS: Moving a boto3 EC2 MCP Server to the MCP Python SDK 2.x A developer documented migrating an AWS EC2-hosted MCP server for managing Gemma 4 E2B on a vLLM deployment from the MCP Python SDK 1.x (FastMCP) to 2.x (MCPServer). The migration was prompted by an unbound mcp dependency resolving to 2.2.0, which broke imports with a ModuleNotFoundError for mcp.server.fastmcp; the writeup covers the rename, camelCase-to-snake_case field changes, and a pip conflict warning from strands-agents requiring mcp<2.2. The server's 15 async tools and 27 tests were updated to run on the 2.x line. This article provides a step by step migration guide for an AWS MCP server from the MCP Python SDK 1.x FastMCP to 2.x MCPServer . The server manages Gemma 4 E2B on an Amazon EC2 G5g instance, a Graviton2 host with an NVIDIA T4G GPU, and a suite of Python MCP tools built on boto3 simplifies management of the vLLM hosted deployment. https://github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-g5g-2b https://github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-g5g-2b The rig's requirements.txt listed mcp with no version bound. Once the machine's Python moved to mcp 2.2.0, the server stopped importing: python python3 -c "import server" ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer from mcp.server.mcpserver import MCPServer and other APIs changed; see the migration guide at https://py.sdk.modelcontextprotocol.io/v2/migration/ fastmcp-renamed-to-mcpserver or pin 'mcp<2' to keep running v1 code. In an MCP client this surfaces less helpfully: the server is launched, dies on the import, and stdio closes before the handshake. The same code on mcp 1.30.0 passes all 27 of its tests. ✅ The error message names both fixes, so the first decision is which one. | | Pin mcp<2 | Migrate to MCPServer | |---|---|---| | Code change | none | import, class name, tests | | Where the fix lives | every interpreter that runs the server | the repository | | Shared Python | holds back mcp for everything on it | nothing global changes | | Future fixes | the v1 maintenance line | the current line | The migration guide says the v1.x line keeps receiving critical bug fixes and security patches, so pinning is legitimate. This rig migrates because every project here installs into one system Python, with no virtualenvs , and the deployment image uses the same layout. That shared Python has an AWS-specific wrinkle. Upgrading mcp on it drew this from pip: strands-agents 1.55.0 requires mcp<2.2, =1.23.0, but you have mcp 2.2.0 which is incompatible. Successfully installed mcp-2.2.0 mcp-types-2.2.0 pip installs anyway and only warns. If Strands Agents shares an interpreter with your MCP server, check its mcp bound before choosing a 2.x release, or give the two separate interpreters. Requires-Python =3.10 gpu-vllm-g5g-2b/ as your working directory pip install -r requirements.txt done against the interpreter your MCP client launches ruff on the path for make lint No AWS credentials are needed for any step here. The tests are offline by design, and the stdio check only lists tools, which never runs a handler or calls boto3. The migration guide's table of changes most projects hit, against this server — stdio transport, 15 @mcp.tool tools, boto3 for every AWS call, and no client code: | Change | First symptom | This server | |---|---|---| | FastMCP renamed to MCPServer | No module named 'mcp.server.fastmcp' | ❌ hit | | camelCase fields renamed to snake case | 'Tool' object has no attribute 'inputSchema' | ❌ hit, in the tests | | httpx replaced by httpx2 | No module named 'httpx' | ⚠️ exposed, already declared | | Sync handlers run on a worker thread | get running loop raises in a def handler | ✅ every handler is async | | MCP env no longer read into settings | settings silently ignored | ✅ MCP SERVER NAME is the rig's own | The first row is the one everybody hits. The second is the one this article is about, because the companion Cloud Run migration never saw it. Measure before editing: grep -c "^@mcp\.\ tool\|resource\|prompt\ " server.py grep -A1 "^@mcp\." server.py | grep -c "^async def" grep -A1 "^@mcp\." server.py | grep -c "^def" grep -n "get running loop\|asyncio.run " server.py || echo " no matches " grep -n "^import httpx" server.py; grep -n "^httpx" requirements.txt grep -n "Hint\b\|inputSchema\|outputSchema\|mimeType" tests/ .py python 15 15 0 no matches 26:import httpx 2:httpx 43: name for name, tool in self.tools.items if tool.annotations.destructiveHint 53: schema = self.tools name .inputSchema "properties" 15 handlers, all async , none touching the event loop, and httpx declared on its own line. The last grep is the one a FastMCP-only checklist misses: two camelCase attribute reads in the test suite. One more grep hit deserves a look. grep -n "MCP " server.py finds MCP SERVER NAME . v2 no longer reads MCP environment variables into server settings, but this one is the rig's own: it is read with os.getenv and passed in as the server's name, so nothing changes. The whole change to server.py : python -from mcp.server.fastmcp import FastMCP +from mcp.server.mcpserver import MCPServer from mcp.types import ToolAnnotations ... MCP SERVER NAME = os.getenv "MCP SERVER NAME", RIG NAME -mcp = FastMCP MCP SERVER NAME +mcp = MCPServer MCP SERVER NAME READ ONLY = ToolAnnotations readOnlyHint=True, idempotentHint=True WRITE = ToolAnnotations destructiveHint=False DESTRUCTIVE = ToolAnnotations destructiveHint=True Make both line changes in one edit. An editor that runs ruff check --fix on save deletes an import that is momentarily unused, and the next edit leaves MCPServer undefined. The three ToolAnnotations ... lines stay exactly as they are, camelCase and all. That is deliberate, and Step 4 shows why it is safe. python3 -m unittest discover -s tests ERROR: test annotations test server.ToolCatalogTests.test annotations AttributeError: 'ToolAnnotations' object has no attribute 'destructiveHint'. Did you mean: 'destructive hint'? ERROR: test launch defaults to spot and build test server.ToolCatalogTests.test launch defaults to spot and build AttributeError: 'Tool' object has no attribute 'inputSchema'. Did you mean: 'input schema'? FAIL: test skill is complete in both copies test server.RepoHygieneTests.test skill is complete in both copies Ran 27 tests in 0.012s FAILED failures=1, errors=2 The server imports and registers its tools. What broke is Python code that reads protocol models, and here that code is the test suite. The failing test is the one that guards EC2 termination. test annotations asserts that exactly two tools, stop g5g instance and terminate g5g instance , carry destructiveHint . On an AWS server that flag is how a client knows a tool can end an instance and destroy its root volume. A migration that deleted this test to get green would remove the check that matters most. The third failure is this rig's own: a test that the generated skill copies match server.py . It clears in Step 5. v2 renamed every protocol model field to snake case for Python attribute access. The fix is two identifiers: - name for name, tool in self.tools.items if tool.annotations.destructiveHint + name for name, tool in self.tools.items if tool.annotations.destructive hint ... - schema = self.tools name .inputSchema "properties" + schema = self.tools name .input schema "properties" Why the ToolAnnotations destructiveHint=True constructors in server.py did not need touching: ToolAnnotations destructiveHint=True .destructive hint True ToolAnnotations destructiveHint=True .destructiveHint AttributeError: 'ToolAnnotations' object has no attribute 'destructiveHint' Tool ... .model dump exclude none=True {'name': 'x', 'input schema': {'type': 'object'}} Tool ... .model dump by alias=True, exclude none=True {'name': 'x', 'inputSchema': {'type': 'object'}} Constructors accept both spellings; attribute access is snake case only. The last two lines are the trap the guide warns about: a plain model dump now emits snake case keys that other MCP implementations will not recognise, with no error. If your server serialises protocol models itself, add by alias=True . This one does not. Code that imports mcp.server.mcpserver cannot run on 1.x: -mcp +mcp =2,<3 The upper bound is the migration guide's own example: meet 3.x on purpose, not by pip install . Keep httpx on its own line in the same file. v2 depends on httpx2 instead, so a server that imports httpx without declaring it loses it on a fresh install, with a traceback that never mentions mcp. This rig also ships its server as a skill, with copies of server.py and requirements.txt under skills/ . make skill regenerates them, which clears the third failure. If your MCP server is vendored anywhere, into a container build context or a Lambda package, refresh that copy too. make lint ruff check server.py refresh skill.py tests All checks passed lint OK python3 -m unittest discover -s tests ---------------------------------------------------------------------- Ran 27 tests in 0.011s OK 27 tests, the same count that passed on mcp 1.30.0 before the change. ✅ Unit tests call Python. A client speaks JSON-RPC over stdio, and the wire is where the camelCase question gets its real answer. Hold stdin open with sleep , or the server sees end-of-input and exits after the first reply: { printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'; sleep 5; } \ | python3 server.py 2 /dev/null The responses are JSON; summarised: initialize OK: name='gpu-vllm-g5g-2b' version='' proto 2025-06-18 tools/list OK: 15 tools - check g5g quotas, create g5g instance, get build progress, get deployment config, ... wire keys on terminate g5g instance: 'annotations', 'description', 'inputSchema', 'name', 'outputSchema', 'title' annotations on the wire: {"destructiveHint": true} destructive on the wire: 'stop g5g instance', 'terminate g5g instance' 🟢 All 15 tools, and the wire is still camelCase: inputSchema , destructiveHint . The same two tools are marked destructive. Clients see no change. The rename is visible only to Python that reads the models. Note version='' . In v2 a server that does not pass a version reports an empty string instead of the installed SDK's version. Pass version="..." to MCPServer ... if anything displays it. boto3 is synchronous, and that is the change most AWS MCP servers should look at hardest. This rig was already safe. Every tool is async def , and each boto3 call goes through one helper: python async def call func, kwargs : return await asyncio.to thread func, kwargs So a slow EC2 or SSM call runs on a worker thread in v1 and v2 alike. v2's change to sync handlers does nothing here. The common shape is different: a plain def tool that calls ec2.describe instances directly. The migration guide says v1 ran such a handler inline on the event loop, so one slow AWS call stalled every other request on the server, and v2 runs it on a worker thread. That is a free concurrency gain for boto3 code. The only thing it breaks is code in a def handler that expects the loop's thread, such as asyncio.get running loop . The guide also points out the reverse: an async def tool that calls boto3 without a thread still blocks the loop in both versions. Upgrading one shared Python moves every MCP server on it at once: grep -l "from mcp.server.fastmcp import" /server.py | wc -l grep -l "from mcp.server.mcpserver import" /server.py 37 gpu-2B-cloudrun-devops-agent/server.py local-llamacpp-1650ti-2b-q4 0/server.py gpu-vllm-g5g-2b/server.py 37 servers in the monorepo still import FastMCP, 13 of them EC2 or Inferentia rigs, and none of them starts on this Python until it gets the same rename. That is the cost of migrating over pinning on a shared interpreter, paid up front. The per-server change is small enough to make paying it reasonable. exposure grep -rn "mcp.server.fastmcp" . grep -A1 "^@mcp\." server.py | grep -c "^def" grep -n "Hint\b\|inputSchema\|outputSchema\|mimeType" tests/ .py grep -n "^import httpx" server.py; grep -n "^httpx" requirements.txt the rename, in ONE edit from mcp.server.fastmcp import FastMCP - from mcp.server.mcpserver import MCPServer FastMCP "name" - MCPServer "name" attribute reads: .destructiveHint - .destructive hint, .inputSchema - .input schema constructors: ToolAnnotations destructiveHint=True still works, leave it requirements.txt: mcp - mcp =2,<3, and declare httpx if you import it make lint && python3 -m unittest discover -s tests stdio smoke test: hold stdin open { printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"p","version":"0"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'; sleep 5; } | python3 server.py 2 /dev/null The goal of this article was to give an AWS EC2 MCP server basic MCP Python SDK 2.x support without changing what it does or what its clients see. The key to the solution was auditing the tests as well as the server, because on this rig the SDK's snake case change broke the test suite and not the server. The migration results were: ToolAnnotations ... constructors stayed destructive hint and input schema tools/list still sends inputSchema and destructiveHint , and the same two tools are marked destructive asyncio.to thread , so v2's worker-thread change was a no-op strands-agents 1.55.0 outside its mcp<2.2 bound and stopped 37 other FastMCP servers until they are migrated Scope: mcp 2.2.0 on Python 3.14.7, with 1.30.0 as the v1 reference, boto3 1.43.90, ruff 0.16.7, on one Debian workstation. Offline unit tests and one stdio handshake; no EC2 instance was provisioned, because nothing in the migration changes an AWS call. The deployment the tools manage is unchanged and is covered in the rig's earlier G5g article. The strategy for using MCP for migrating an AWS MCP server to the MCP SDK 2.x was validated with an incremental step by step approach. mcp 2.2.0 mcp-types 2.2.0 , Python 3.14.7, boto3 1.43.90, ruff 0.16.7.