My requirements.txt Is Pinned. My MCP Server's Actual Contract Isn't, and Nothing Would Catch It Changing.
Back on 2026-07-14 I found and fixed a real landmine in this repo: requirements.txt had mcp[cli] with no version constraint at all. Any fresh install could pull in a breaking major version with zero warning. I pinned it to mcp[cli]>=1.28.0,<2.0.0 and moved on, feeling like I'd closed the gap. I hadn't. I'd only pinned the library . The actual contract my MCP server exposes to any agent that connects to it — the tool names, parameter shapes, and descriptions an LLM reads to decide how to call my code — isn't a version string anywhere. It's generated fresh, every time the server boots, from whatever my function signatures and docstrings happen to say at that moment. Nothing pins that. Nothing diffs it. Nothing tests it. What actually generates the contract My server ( server.py ) is a FastMCP app with plain @mcp.tool() -decorated functions: @mcp.tool () def create_article ( title : str , body_markdown : str , tags : list [ str ] = None , published : bool = False ) -> dict : """ Create a new DEV.to article. Returns id and url. """ payload = { " article " : { " title " : title , " body_markdown " : body_markdown , " published " : published }} if tags : payload [ " article " ][ " tags " ] = tags result = _dev ( " /articles " , method = " POST " , data = payload ) return { " id " : result [ " id " ], " url " : result . get ( " url " ), " published " : result . get ( " published " )} FastMCP inspects that signature at import time and builds the JSON Schema an agent actually sees — parameter names, types, which ones are required, and the docstring as the tool's description. I never write that schema by hand and I never check it in anywhere. It's derived, every run, from source that I edit for completely unrelated reasons. That's the gap. requirements.txt pinning stops FastMCP's own behavior from shifting under me between installs. It does nothing about my behavior shifting the schema FastMCP generates from my code, on every single commit, with no separate review step. Where