{"slug": "fastmcp-is-now-mcpserver-on-aws-moving-a-boto3-ec2-mcp-server-to-the-mcp-python", "title": "FastMCP Is Now MCPServer on AWS: Moving a boto3 EC2 MCP Server to the MCP Python SDK 2.x", "summary": "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.", "body_md": "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.\n\n[https://github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-g5g-2b](https://github.com/xbill9/gemma4-dev/tree/main/gpu-vllm-g5g-2b)\n\nThe 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:\n\n``` python\npython3 -c \"import server\"\nModuleNotFoundError: 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.\n```\n\nIn 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. ✅\n\nThe error message names both fixes, so the first decision is which one.\n\n|  | Pin `mcp<2` | Migrate to `MCPServer` | \n|---|---|---|\n| Code change | none | import, class name, tests | \n| Where the fix lives | every interpreter that runs the server | the repository | \n| Shared Python | holds back `mcp` for everything on it | nothing global changes | \n| Future fixes | the v1 maintenance line | the current line | \n\nThe 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.\n\nThat shared Python has an AWS-specific wrinkle. Upgrading `mcp` on it drew this from pip:\n\n```\nstrands-agents 1.55.0 requires mcp<2.2,>=1.23.0, but you have mcp 2.2.0 which is incompatible.\nSuccessfully installed mcp-2.2.0 mcp-types-2.2.0\n```\n\npip 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.\n\n`Requires-Python >=3.10`\n`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`\nNo 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.\n\nThe 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:\n\n| Change | First symptom | This server | \n|---|---|---|\n| `FastMCP` renamed to`MCPServer` | `No module named 'mcp.server.fastmcp'` | ❌ hit | \n| camelCase fields renamed to snake_case | `'Tool' object has no attribute 'inputSchema'` | ❌ hit, in the tests | \n| `httpx` replaced by`httpx2` | `No module named 'httpx'` | ⚠️ exposed, already declared | \n| Sync handlers run on a worker thread | `get_running_loop()` raises in a`def` handler | ✅ every handler is `async` | \n| `MCP_*` env no longer read into settings | settings silently ignored | ✅ `MCP_SERVER_NAME` is the rig's own | \n\nThe 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.\n\nMeasure before editing:\n\n```\ngrep -c \"^@mcp\\.\\(tool\\|resource\\|prompt\\)\" server.py\ngrep -A1 \"^@mcp\\.\" server.py | grep -c \"^async def\"\ngrep -A1 \"^@mcp\\.\" server.py | grep -c \"^def\"\ngrep -n \"get_running_loop\\|asyncio.run(\" server.py || echo \"(no matches)\"\ngrep -n \"^import httpx\" server.py; grep -n \"^httpx\" requirements.txt\ngrep -n \"Hint\\b\\|inputSchema\\|outputSchema\\|mimeType\" tests/*.py\npython\n15\n15\n0\n(no matches)\n26:import httpx\n2:httpx\n43:            name for name, tool in self.tools.items() if tool.annotations.destructiveHint\n53:            schema = self.tools[name].inputSchema[\"properties\"]\n```\n\n15 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.**\n\nOne 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.\n\nThe whole change to `server.py`:\n\n``` python\n-from mcp.server.fastmcp import FastMCP\n+from mcp.server.mcpserver import MCPServer\n from mcp.types import ToolAnnotations\n ...\n MCP_SERVER_NAME = os.getenv(\"MCP_SERVER_NAME\", RIG_NAME)\n-mcp = FastMCP(MCP_SERVER_NAME)\n+mcp = MCPServer(MCP_SERVER_NAME)\n READ_ONLY = ToolAnnotations(readOnlyHint=True, idempotentHint=True)\n WRITE = ToolAnnotations(destructiveHint=False)\n DESTRUCTIVE = ToolAnnotations(destructiveHint=True)\n```\n\nMake 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.\n\nThe three `ToolAnnotations(...)` lines stay exactly as they are, camelCase and all. That is deliberate, and Step 4 shows why it is safe.\n\n```\npython3 -m unittest discover -s tests\nERROR: test_annotations (test_server.ToolCatalogTests.test_annotations)\nAttributeError: 'ToolAnnotations' object has no attribute 'destructiveHint'. Did you mean: 'destructive_hint'?\nERROR: test_launch_defaults_to_spot_and_build (test_server.ToolCatalogTests.test_launch_defaults_to_spot_and_build)\nAttributeError: 'Tool' object has no attribute 'inputSchema'. Did you mean: 'input_schema'?\nFAIL: test_skill_is_complete_in_both_copies (test_server.RepoHygieneTests.test_skill_is_complete_in_both_copies)\nRan 27 tests in 0.012s\nFAILED (failures=1, errors=2)\n```\n\nThe server imports and registers its tools. What broke is Python code that reads protocol models, and here that code is the test suite.\n\n**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.\n\nThe third failure is this rig's own: a test that the generated skill copies match `server.py`. It clears in Step 5.\n\nv2 renamed every protocol model field to snake_case for Python attribute access. The fix is two identifiers:\n\n```\n-            name for name, tool in self.tools.items() if tool.annotations.destructiveHint\n+            name for name, tool in self.tools.items() if tool.annotations.destructive_hint\n ...\n-            schema = self.tools[name].inputSchema[\"properties\"]\n+            schema = self.tools[name].input_schema[\"properties\"]\n```\n\nWhy the `ToolAnnotations(destructiveHint=True)` constructors in `server.py` did not need touching:\n\n```\n>>> ToolAnnotations(destructiveHint=True).destructive_hint\nTrue\n>>> ToolAnnotations(destructiveHint=True).destructiveHint\nAttributeError: 'ToolAnnotations' object has no attribute 'destructiveHint'\n>>> Tool(...).model_dump(exclude_none=True)\n{'name': 'x', 'input_schema': {'type': 'object'}}\n>>> Tool(...).model_dump(by_alias=True, exclude_none=True)\n{'name': 'x', 'inputSchema': {'type': 'object'}}\n```\n\nConstructors 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.\n\nCode that imports `mcp.server.mcpserver` cannot run on 1.x:\n\n```\n-mcp\n+mcp>=2,<3\n```\n\nThe 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.\n\nThis 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.\n\n```\nmake lint\nruff check server.py refresh_skill.py tests\nAll checks passed!\nlint OK\npython3 -m unittest discover -s tests\n----------------------------------------------------------------------\nRan 27 tests in 0.011s\n\nOK\n```\n\n27 tests, the same count that passed on mcp 1.30.0 before the change. ✅\n\nUnit 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:\n\n```\n{ printf '%s\\n' \\\n  '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"probe\",\"version\":\"0\"}}}' \\\n  '{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}' \\\n  '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}'; sleep 5; } \\\n  | python3 server.py 2>/dev/null\n```\n\nThe responses are JSON; summarised:\n\n```\ninitialize OK: name='gpu-vllm-g5g-2b' version='' proto 2025-06-18\ntools/list OK: 15 tools -> check_g5g_quotas, create_g5g_instance, get_build_progress, get_deployment_config, ...\nwire keys on terminate_g5g_instance: ['annotations', 'description', 'inputSchema', 'name', 'outputSchema', 'title']\nannotations on the wire: {\"destructiveHint\": true}\ndestructive on the wire: ['stop_g5g_instance', 'terminate_g5g_instance']\n```\n\n🟢 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.\n\nNote `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.\n\nboto3 is synchronous, and that is the change most AWS MCP servers should look at hardest.\n\nThis rig was already safe. Every tool is `async def`, and each boto3 call goes through one helper:\n\n``` python\nasync def _call(func, **kwargs):\n    return await asyncio.to_thread(func, **kwargs)\n```\n\nSo 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.\n\nThe 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.\n\nUpgrading one shared Python moves every MCP server on it at once:\n\n```\ngrep -l \"from mcp.server.fastmcp import\" */server.py | wc -l\ngrep -l \"from mcp.server.mcpserver import\" */server.py\n37\ngpu-2B-cloudrun-devops-agent/server.py\nlocal-llamacpp-1650ti-2b-q4_0/server.py\ngpu-vllm-g5g-2b/server.py\n```\n\n37 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.\n\n```\n# exposure\ngrep -rn \"mcp.server.fastmcp\" .\ngrep -A1 \"^@mcp\\.\" server.py | grep -c \"^def\"\ngrep -n \"Hint\\b\\|inputSchema\\|outputSchema\\|mimeType\" tests/*.py\ngrep -n \"^import httpx\" server.py; grep -n \"^httpx\" requirements.txt\n\n# the rename, in ONE edit\n#   from mcp.server.fastmcp import FastMCP  ->  from mcp.server.mcpserver import MCPServer\n#   FastMCP(\"name\")                         ->  MCPServer(\"name\")\n# attribute reads: .destructiveHint -> .destructive_hint, .inputSchema -> .input_schema\n# constructors:    ToolAnnotations(destructiveHint=True) still works, leave it\n# requirements.txt: mcp -> mcp>=2,<3, and declare httpx if you import it\n\nmake lint && python3 -m unittest discover -s tests\n\n# stdio smoke test: hold stdin open\n{ printf '%s\\n' '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"p\",\"version\":\"0\"}}}' \\\n                '{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}' \\\n                '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}'; sleep 5; } | python3 server.py 2>/dev/null\n```\n\nThe 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:\n\n`ToolAnnotations(...)` constructors stayed`destructive_hint` and `input_schema`\n`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\nScope: 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.\n\nThe 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.\n\n*mcp 2.2.0 (mcp-types 2.2.0), Python 3.14.7, boto3 1.43.90, ruff 0.16.7.*", "url": "https://wpnews.pro/news/fastmcp-is-now-mcpserver-on-aws-moving-a-boto3-ec2-mcp-server-to-the-mcp-python", "canonical_source": "https://dev.to/aws-builders/fastmcp-is-now-mcpserver-on-aws-moving-a-boto3-ec2-mcp-server-to-the-mcp-python-sdk-2x-42gk", "published_at": "2026-09-11 15:57:49+00:00", "updated_at": "2026-09-11 16:11:33.944872+00:00", "lang": "en", "topics": ["ai-tools", "ai-infrastructure", "developer-tools", "large-language-models", "mlops"], "entities": ["AWS", "Amazon EC2", "boto3", "FastMCP", "MCPServer", "MCP Python SDK", "Gemma 4 E2B", "vLLM"], "alternates": {"html": "https://wpnews.pro/news/fastmcp-is-now-mcpserver-on-aws-moving-a-boto3-ec2-mcp-server-to-the-mcp-python", "markdown": "https://wpnews.pro/news/fastmcp-is-now-mcpserver-on-aws-moving-a-boto3-ec2-mcp-server-to-the-mcp-python.md", "text": "https://wpnews.pro/news/fastmcp-is-now-mcpserver-on-aws-moving-a-boto3-ec2-mcp-server-to-the-mcp-python.txt", "jsonld": "https://wpnews.pro/news/fastmcp-is-now-mcpserver-on-aws-moving-a-boto3-ec2-mcp-server-to-the-mcp-python.jsonld"}}