← Back to Blog

Introspection that imports your code, on purpose

·5 min read

An agent asks what APIs exist in this project. There are two ways to answer: parse Python files hoping to infer decorator semantics, or import the module and ask the runtime. pyRPC chose the runtime years ago when it built the registry; the MCP simply refuses to reinvent a worse version.

Why static analysis loses here

pyRPC procedures are compiled objects. At decoration time the framework inspects signatures, pre-builds pydantic TypeAdapters per parameter, captures docstrings, records async-ness and kind. Conditional registration, dynamic module layout, and Django's views pattern all fall out naturally at import. A parser sees none of it. Worse, a parser's answer diverges from what the server actually serves, and divergence between believed and real APIs is precisely the bug class type-safe RPC exists to kill.

The chain we reused

The MCP context loader composes five pieces that already existed: find_config walks up the tree for pyrpc.json, parse_backend validates the nested backend section into a frozen BackendSpec, resolve_types_module picks the module whose import registers procedures (with Django required to declare one), importlib brings it into the process, and get_registry_schema serializes the router. Zero new introspection logic was written; the MCP is a lens on machinery the dev server already trusts.

The consequence worth stating

Importing user code is privileged. It runs in the user's environment with the user's permissions because the user's own client launched the process, which is the same contract as running pyrpc dev. What the MCP adds is discipline around failure: if the module will not import, the agent receives a structured error naming the module, the exception, and the two config fields that might be wrong, rather than half of a traceback. Ground truth is worth the responsibility, and the responsibility is handled explicitly rather than assumed.