mcp-library-builder
by linga009
README.md
# mcp-library-builder
[](https://github.com/linga009/mcp-library-builder/actions/workflows/ci.yml)
[](LICENSE)
[](pyproject.toml)
**Turn any Python library into a real, standalone MCP server โ automatically.**
Not toy examples (a calculator, a weather lookup). Real libraries: `math`,
`pandas`, `requests`, `simpy`, `keras`, and anything else you can `pip
install`.
## Why
Tool/function calling exists because LLMs are unreliable at precise
computation and at tracking state accurately. This project takes that to its
logical conclusion: an LLM's job is a one-time build step โ reading a
library's structure and generating a clean MCP server for it โ never a
runtime dependency. Once generated, a server is plain, deterministic Python;
using it involves no LLM computation, guessing, or hallucinated state.
## How it works
```mermaid
flowchart LR
lib["Python library\n(math, pandas, ...)"] --> introspect["Introspect\ninspect"]
introspect --> classify["Classify\nfunction / constructor / method"]
classify --> curate["Curate ๐ง \nLLM picks the useful tools"]
curate --> generate["Generate\nJinja2 templates"]
generate --> validate["Validate\nimport-check"]
validate --> out["Real MCP server\nplain, deterministic Python"]
style curate fill:#fff3cd,stroke:#997404,stroke-width:2px
```
Only the highlighted **Curate** step ever touches an LLM โ deciding which of
a library's functions are common enough to be first-class tools. Every other
step (introspecting the real library, generating the server, checking that
it actually imports) is deterministic Python with zero hallucination risk by
construction, since it only ever reads and renders real code.
## Status
Phase 1 (this codebase): the full pipeline works end-to-end for stateless
libraries โ including keyword-only parameters, `*args`/`**kwargs`, optional
parameters, and libraries containing a function named after the module itself
(or after the generated server's own bindings). It has been swept against 77
real standard-library modules, all of which generate importable servers, and
against combinations of up to 10 libraries in one aggregated bundle (288
tools generated and registered correctly in that run). Stateful,
object-oriented libraries (`simpy`, `keras`) are Phase 2.
## Tried on real libraries
Past the standard library, this has been run against two genuinely
complicated real combinations, unfiltered โ including the failures, which
are more informative than the successes:
- **A real ML-workflow bundle:** `numpy` + `scipy` + `pandas` + `scikit-learn`
+ `mlflow`, combined into one server with `build_aggregated_server`. First
run: **210 real tools, one process**, everything called back computed a
correct answer (`numpy_zeros`, `pandas_unique`, `sklearn_get_config`, ...).
It also found two real bugs, both since fixed:
- `mlflow.register_prompt` has a legitimate `model_config` parameter that
collides with a name Pydantic reserves internally, which briefly took
the *entire* build down. Fixed by renaming just that one parameter in
the generated wrapper's own signature (`model_config` โ
`model_config_param`) rather than dropping the function โ same fix
applied to the pre-existing underscore-prefix case
(`hashlib.file_digest`'s `_bufsize` โ `bufsize`), which used to be
dropped outright and is now a real, callable tool too.
- numpy's most-used functions (`mean`, `sum`, `dot`, `median`, ...) were
entirely invisible โ not even logged โ because numpy wraps them in a
private dispatcher type `inspect.isfunction`/`isbuiltin` doesn't
recognize. Broadening the check to `inspect.isroutine` (still verified
to exclude ordinary callable objects) took **numpy alone from 60 to 245
introspectable functions** in this same bundle. `mean`/`sum` themselves
still don't make the final tool list โ they have a separate,
already-handled problem (a sentinel default that isn't a literal, same
class of issue as `pandas.read_csv`) โ but they're now visibly skipped
with a reason instead of silently absent, and most of their siblings
(`dot`, `median`, `transpose`, `reshape`, ...) are fully recovered.
Rerun with both fixes: **397 tools, one process.** (`tensorflow`/`keras`
still couldn't be included โ no PyPI build of TensorFlow exists yet for
the Python version this was run on, and Keras won't import without a
working TensorFlow backend. Not a limitation of this tool.)
- **The snake bites its own tail:** this project's own `generator/curation.py`
imports `langchain.chat_models.init_chat_model` to power its LLM step.
Pointed at `langchain.chat_models` and `langgraph.graph` โ the exact
modules this tool itself depends on โ it correctly generated
`langchain_chat_models_init_chat_model` and `langgraph_graph_add_messages`
as real, working tools. `add_messages`, called with real
`langchain_core.messages` objects, returned the exact merged result its
own docstring documents.
See [Limitations](#limitations) for what these runs still reveal about where
the tool's real edges are.
## Install
```bash
pip install -e ".[dev]"
cp .env.example .env # fill in an API key for curation
```
## Usage
```bash
python -m generator.server
```
This starts the generator's own MCP server, exposing two tools:
- `build_mcp_server(library_name: str) -> str` โ generates a server for the
named importable library and returns the path to the generated file. Output
goes to `./generated_servers/<library_name>/server.py`, relative to the
directory you run the tool from.
- `build_aggregated_server(library_names: list[str], bundle_name: str) -> str`
โ generates one combined server spanning several importable libraries, so a
project needing multiple libraries gets a single MCP process and a single
client config entry instead of one per library. Each library's tools are
prefixed with its own name (`math_sqrt`, `pandas_read_csv`, ...) to avoid
collisions between libraries. A library that fails is skipped rather than
failing the whole build; output goes to
`./generated_servers/<bundle_name>/server.py`.
```mermaid
flowchart TB
subgraph withoutAgg["Without aggregation โ one process per library"]
direction LR
m1["math server"]
p1["pandas server"]
r1["requests server"]
end
subgraph withAgg["One build_aggregated_server call โ one process"]
direction LR
bundle["math_sqrt ยท pandas_read_csv ยท requests_get ยท ..."]
end
withoutAgg -.-> withAgg
```
Generation is validated before it is reported as successful: the curated
function list is checked against what introspection actually found, and the
generated module is imported. A run that would produce unusable output raises
`generator.server.GenerationError` rather than handing back a path to code
nobody checked.
Individual functions that cannot be expressed as MCP tools are skipped so the
rest of the library still generates โ a default value that is not a Python
literal (`shutil.copytree`'s `copy_function=<function copy2 at 0x...>`), or a
signature `inspect` cannot read at all (`math.log`). A parameter name MCP
tool registration would otherwise reject (a leading `_`, or a collision with
a Pydantic-reserved name) is renamed rather than costing the function โ
see [Limitations](#limitations). Each skip that does happen is reported at
INFO on the `generator.introspection` logger, with the reason.
## What a generated server looks like
`build_mcp_server("math")`, curated down to just `sqrt`, produces this โ
real output, not a mockup:
```python
# -*- coding: utf-8 -*-
"""Generated MCP server for math.
DO NOT EDIT BY HAND โ regenerate via generator.build_mcp_server().
"""
from mcp.server.mcpserver import MCPServer as _MCPServer
import math as _lib
_server = _MCPServer(name="math-mcp-server")
@_server.tool()
def sqrt(x: float) -> float:
'Return the square root of x.'
return _lib.sqrt(x)
if __name__ == "__main__":
_server.run()
```
No LLM calls anywhere in that file โ it's a plain, deterministic wrapper
around the real `math.sqrt`, readable and auditable like any other generated
code. An aggregated bundle looks the same, just with one `import ... as
_<library>` line and one `@_server.tool()` function per promoted function
across every requested library, each tool name prefixed with its own
library's name (`math_sqrt`, `time_time`, ...).
## Using a generated server
The output is a normal stdio MCP server โ point any MCP client at it the
same way you'd point it at a hand-written one. For Claude Desktop or Claude
Code, add it to the client's MCP server config:
```json
{
"mcpServers": {
"math": {
"command": "python",
"args": ["generated_servers/math/server.py"]
}
}
}
```
For an aggregated bundle, point at `generated_servers/<bundle_name>/server.py`
instead โ one config entry covers every library in that bundle.
## Limitations
Found through actual use, not guessed at upfront:
- **Only module-level plain functions are introspected โ no classes, no
methods, no state.** This is why a library organized around objects rather
than free functions can look nearly empty: `scipy`'s bare top level yields
1 function, `sklearn`'s yields 5 (config utilities, not `train_test_split`
or any estimator), and `langchain`/`langgraph`'s yield **0** โ their real
API lives one or two levels down, in submodules or classes this tool
doesn't walk yet. Point it at the specific submodule you actually want
(`scipy.stats`, `langchain.chat_models`) rather than the bare package name.
Stateful/object-oriented support is the planned Phase 2.
- **A parameter name MCP tool registration would otherwise reject โ a
leading `_`, or a collision with a Pydantic-reserved name like
`model_config` โ is renamed in the generated wrapper's own signature
rather than costing the whole function.** `hashlib.file_digest`'s
`_bufsize` becomes `bufsize`; `mlflow.register_prompt`'s `model_config`
becomes `model_config_param`; the real call still uses the library's real
keyword. Only still skipped (and logged) if the rename itself would
collide with another parameter of the same function.
- **A default value that isn't a literal Python expression still costs the
function.** `shutil.copytree`'s `copy_function=<function copy2 ...>`,
numpy's `keepdims=<no value>` sentinel on many reduction functions,
`pandas.read_csv`'s `sep=<no_default>` โ none of these can be renamed away,
since the problem is the value, not the name. Safely skipped and logged;
one bad function never takes down the rest of a library or bundle, but the
promoted tool list is sometimes narrower than the library's real surface.
- **Curation quality depends on the LLM you point it at.** Every real trial
above used a stand-in that promotes everything introspection found, to
stress-test the generation pipeline itself rather than an LLM's judgment
about what's "useful" โ that judgment call hasn't been evaluated yet.
## Contributing
This was built out of curiosity, to see if it'd be useful to someone โ
consider this an open invitation rather than a finished product. Concretely
useful things to try or pick up:
- **Try it on a library not listed above and open an issue with what
happened** โ especially the failures. A silent skip nobody logged, or a
new whole-build-breaking collision, is exactly the kind of thing real use
finds and a test suite doesn't โ both `model_config` and numpy's
dispatcher-wrapped functions were found this way and are now fixed.
- **Submodule walking** โ so `build_mcp_server("scipy.stats")` doesn't
require knowing the submodule name upfront.
- **Phase 2: class/state support** โ the biggest single gap. Most of what
makes `pandas`, `sklearn`, and `keras` actually useful is object-oriented,
and none of it is reachable yet.
- Anything in [Limitations](#limitations) above.
PRs and issues both welcome.
## Running the tests
```bash
pip install -e ".[dev]"
pytest -v
```
137 tests, real behavior throughout: generated code is actually written to
disk, imported, and called โ nothing is mocked except the LLM curation call
itself. CI runs this same suite, plus linting, against Python 3.10 through
3.14 on every push and pull request.
## Linting
```bash
ruff check .
ruff format generator/ tests/
```
`ruff format` is deliberately scoped to the actual source directories, not
the whole repo โ it also reformats fenced code blocks inside `.md` files,
which would silently drift the README's real, verified generated-server
sample away from being byte-for-byte accurate.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing