·
Last changedVersion:
No hardening report available.
Create a Model Context Protocol (MCP) server in Python with the official MCP Python SDK v2. SDK v2 supports the MCP specification revision 2026-07-28. The same server also serves clients that use earlier revisions.
Copy this checklist and track your progress:
Run these commands:
Then delete main.py. The server goes in server.py.
--no-package, uv makes a src/ package layout, and the tests cannot import server.py.uv init makes the .gitignore file. Keep that file. When the server uses secrets, append lines that exclude env files and other credential files.Start from this template:
Then change the template for the request of the user:
divide with the tools that the user needs. Constrain every argument with types, Pydantic constraints, and allowlists. Use parameterized queries, and build each SQL statement or shell command from fixed text rather than from arguments. Expose narrow tools rather than generic execute or query tools. Use read-only or least-privilege database credentials and narrowly scoped API tokens. Require authorization for sensitive operations.TypedDict when the client needs machine-readable data. The SDK makes the output schema from the return type. Return only the fields the tool needs. Treat text from external APIs or databases as untrusted data, keep it out of tool descriptions, and label or truncate free text from third parties.async def for I/O. The SDK runs a sync tool on a worker thread.@mcp.tool(annotations=ToolAnnotations(read_only_hint=True)) (from mcp.types). Use destructive_hint=True for a destructive tool. Default to read-only tools and read-only credentials. Add a destructive tool only when the user explicitly asks for it. Annotations are hints to the host and enforce nothing, so a destructive tool must get user confirmation before it acts, for example through a Resolve parameter that returns Elicit, as the Gotchas describe, or through a confirm parameter that defaults to a dry run.@mcp.resource("users://{user_id}")) and prompts (@mcp.prompt()) only when the user asks for them.Use stdio by default. Read references/streamable-http.md only when the user asks for a remote server or an HTTP server.
Put test_server.py next to server.py. Write a test for each tool and for each ToolError:
Client needs no subprocess and no port. It connects with the 2026-07-28 revision.mcp package installs anyio, which supplies the anyio marker. Do not add pytest-asyncio.uv run pytest.uv run pytest again.For VS Code, write .vscode/mcp.json:
VS Code launches the command in this file. Review the file before you commit it or trust it in a cloned repository. The absolute path reveals a local directory.
For Claude Desktop, uv run mcp install server.py adds a persistent entry to the Claude Desktop settings outside the project folder, and it can replace an entry with the same name. Explain this to the user first. Run the command only after the user explicitly agrees, has checked for an entry with the same name, and has backed up the settings file.
Tell the user these commands:
uv run server.py starts the stdio server. The server waits for a host on stdin and prints nothing.uv run mcp dev server.py opens the MCP Inspector. The Inspector needs Node.js. The Inspector is optional, and the user starts it. It downloads and runs the Inspector package and a local proxy that can start processes. Use a recent patched release, keep it on localhost with its proxy authentication on, and stop it after use.SDK v2 and the 2026-07-28 revision changed many SDK v1 patterns. Do not copy SDK v1 examples.
FastMCP and mcp.server.fastmcp. Import the server class with from mcp.server import MCPServer.Context, Image, Audio, Resolve, Elicit, ElicitationResult, AcceptedElicitation, Message, UserMessage, and AssistantMessage from mcp.server.mcpserver.MCPServer argument. Give all other constructor arguments as keyword arguments. Their positional order changed in SDK v2.version. If you do not set it, the server reports an empty version.transport, host, port, json_response, stateless_http, transport_security) to run(), not to MCPServer(...).read_only_hint, structured_content, and is_error.ToolError for an error that the model must read. For other exceptions, the model gets only a generic error message. Write each ToolError message by hand. Keep raw exception text, stack traces, file paths, connection strings, credentials, and record data out of it, and log those details to stderr with secrets redacted.MCPError for a tool failure. The client gets a protocol error, not a tool result with is_error. Many hosts do not show this error to the model.print(). Log to stderr with the logging module. Redact credentials, tokens, and user data in tool arguments and results from log messages.server.py, .vscode/mcp.json, tool results, and error messages, and keep any env file out of version control.ctx.elicit(). It fails on a 2026-07-28 connection. For user input during a tool call, annotate a parameter with Resolve(fn). Return Elicit(message, Model) from fn.await ctx.notify_tools_changed(). A 2026-07-28 connection drops ctx.session.send_tool_list_changed().Client(mcp) in a test runs the lifespan again. Read its object with ctx.request_context.lifespan_context.ctx.session.create_message()): Call the LLM provider API directly. Read the provider API key from an environment variable or a secret store, and keep it out of the code and the client configuration.ctx.log(), ctx.info(), and similar methods): Use the standard logging module.ctx.session.list_roots()): Get paths from tool parameters, resource URIs, or the server configuration.Bring Chainguard's hardening pipeline to your first-party skills — custom rulesets, automated remediation, full audit trail.
skills.cgr.dev/public/github.com/github/skills/python-mcp-server-generator:latest
MIT
HARDENING.md
LICENSE
SKILL.md
references/streamable-http.md