pyrestoolbox 3.7.0__tar.gz → 3.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/PKG-INFO +1 -1
  2. pyrestoolbox-3.7.1/mcp/DEPLOYMENT.md +53 -0
  3. pyrestoolbox-3.7.1/mcp/Dockerfile +23 -0
  4. pyrestoolbox-3.7.1/mcp/README.md +87 -0
  5. pyrestoolbox-3.7.1/mcp/pyproject.toml +26 -0
  6. pyrestoolbox-3.7.1/mcp/pyrestoolbox_mcp/__init__.py +3 -0
  7. pyrestoolbox-3.7.1/mcp/pyrestoolbox_mcp/serialize.py +50 -0
  8. pyrestoolbox-3.7.1/mcp/pyrestoolbox_mcp/server.py +295 -0
  9. pyrestoolbox-3.7.1/mcp/tests/test_server.py +183 -0
  10. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyproject.toml +1 -1
  11. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/__init__.py +23 -2
  12. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/brine/_lib_vle_engine.py +98 -22
  13. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/brine/brine.py +11 -12
  14. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/brine.rst +3 -3
  15. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/changelist.rst +4 -0
  16. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/gas.rst +9 -3
  17. pyrestoolbox-3.7.1/pyrestoolbox/docs/index.rst +62 -0
  18. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/layer.rst +2 -2
  19. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/nodal.rst +9 -6
  20. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/oil.rst +7 -8
  21. pyrestoolbox-3.7.1/pyrestoolbox/docs/plyasunov.rst +230 -0
  22. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/simtools.rst +7 -3
  23. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/gas/gas.py +18 -28
  24. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/layer/layer.py +1 -1
  25. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/nodal/nodal.py +2 -2
  26. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/plyasunov/plyasunov_model.py +9 -2
  27. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/zfactor/mod.rs +2 -33
  28. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/.github/workflows/build-wheels.yml +0 -0
  29. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/.github/workflows/tests.yml +0 -0
  30. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/.gitignore +0 -0
  31. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/Cargo.lock +0 -0
  32. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/Cargo.toml +0 -0
  33. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/LICENSE +0 -0
  34. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/MANIFEST.in +0 -0
  35. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/README.rst +0 -0
  36. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/ResToolbox/privacy_policy.md +0 -0
  37. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/benchmark_rust_vs_python.py +0 -0
  38. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/build_pure_python.py +0 -0
  39. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/_accelerator.py +0 -0
  40. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/brine/__init__.py +0 -0
  41. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/brine/_lib_salting_library.py +0 -0
  42. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/classes/__init__.py +0 -0
  43. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/classes/classes.py +0 -0
  44. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/constants/__init__.py +0 -0
  45. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/constants/constants.py +0 -0
  46. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/dca/__init__.py +0 -0
  47. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/dca/dca.py +0 -0
  48. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/dca.rst +0 -0
  49. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/examples.ipynb +0 -0
  50. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/img/bot.png +0 -0
  51. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
  52. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/img/bot_img.png +0 -0
  53. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/img/dry_gas.png +0 -0
  54. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
  55. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/img/influence.png +0 -0
  56. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/img/properties_df.png +0 -0
  57. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/img/sgof.png +0 -0
  58. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/img/swof.png +0 -0
  59. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/library.rst +0 -0
  60. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/matbal.rst +0 -0
  61. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/nodal_examples.ipynb +0 -0
  62. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/nodal_hydrate_demo.ipynb +0 -0
  63. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/recommend.rst +0 -0
  64. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/docs/sensitivity.rst +0 -0
  65. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/gas/__init__.py +0 -0
  66. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/gas/_hydrate.py +0 -0
  67. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/layer/__init__.py +0 -0
  68. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/library/__init__.py +0 -0
  69. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/library/component_library.xlsx +0 -0
  70. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/library/library.py +0 -0
  71. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/matbal/__init__.py +0 -0
  72. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/matbal/matbal.py +0 -0
  73. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/nodal/__init__.py +0 -0
  74. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/__init__.py +0 -0
  75. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/_compressibility.py +0 -0
  76. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/_constants.py +0 -0
  77. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/_correlations.py +0 -0
  78. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/_density.py +0 -0
  79. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/_harmonize.py +0 -0
  80. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/_pvt_class.py +0 -0
  81. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/_rate.py +0 -0
  82. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/_separator.py +0 -0
  83. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/_tables.py +0 -0
  84. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/oil/_utils.py +0 -0
  85. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/plyasunov/__init__.py +0 -0
  86. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/plyasunov/iapws_if97.py +0 -0
  87. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/plyasunov/water_properties.py +0 -0
  88. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/recommend/__init__.py +0 -0
  89. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/recommend/recommend.py +0 -0
  90. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/sensitivity/__init__.py +0 -0
  91. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/sensitivity/sensitivity.py +0 -0
  92. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/shared_fns/__init__.py +0 -0
  93. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/shared_fns/shared_fns.py +0 -0
  94. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/simtools/__init__.py +0 -0
  95. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/simtools/_aquifer.py +0 -0
  96. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/simtools/_decks.py +0 -0
  97. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/simtools/_pvt_tables.py +0 -0
  98. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/simtools/_relperm.py +0 -0
  99. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/simtools/_vfp.py +0 -0
  100. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/simtools/simtools.py +0 -0
  101. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/validate/__init__.py +0 -0
  102. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/pyrestoolbox/validate/validate.py +0 -0
  103. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/setup.cfg +0 -0
  104. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/setup.py +0 -0
  105. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/bessel.rs +0 -0
  106. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/critical_properties/mod.rs +0 -0
  107. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/dca/hyperbolic.rs +0 -0
  108. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/dca/mod.rs +0 -0
  109. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/dca/ransac.rs +0 -0
  110. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/gas_viscosity/mod.rs +0 -0
  111. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/gwr.rs +0 -0
  112. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/lib.rs +0 -0
  113. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/matbal/mod.rs +0 -0
  114. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/matbal/objective.rs +0 -0
  115. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/oil/density.rs +0 -0
  116. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/oil/mod.rs +0 -0
  117. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/pseudopressure.rs +0 -0
  118. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/spycher_pruess/mod.rs +0 -0
  119. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/spycher_pruess/solubility.rs +0 -0
  120. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vle/alpha.rs +0 -0
  121. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vle/bip.rs +0 -0
  122. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vle/components.rs +0 -0
  123. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vle/eos.rs +0 -0
  124. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vle/flash.rs +0 -0
  125. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vle/fugacity.rs +0 -0
  126. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vle/k_init.rs +0 -0
  127. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vle/mod.rs +0 -0
  128. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vle/rachford_rice.rs +0 -0
  129. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vlp/constants.rs +0 -0
  130. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vlp/friction.rs +0 -0
  131. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vlp/holdup_bb.rs +0 -0
  132. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vlp/holdup_gray.rs +0 -0
  133. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vlp/holdup_wg.rs +0 -0
  134. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vlp/ift.rs +0 -0
  135. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vlp/march.rs +0 -0
  136. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vlp/mod.rs +0 -0
  137. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vlp/pvt_helpers.rs +0 -0
  138. {pyrestoolbox-3.7.0 → pyrestoolbox-3.7.1}/src/vlp/static_column.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrestoolbox
3
- Version: 3.7.0
3
+ Version: 3.7.1
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
@@ -0,0 +1,53 @@
1
+ # pyrestoolbox-mcp: deployment note for the Mawson platform team
2
+
3
+ ## What this is
4
+
5
+ An MCP (Model Context Protocol) server exposing pyResToolbox, a petroleum
6
+ engineering calculation library (gas/oil/brine PVT, nodal analysis, decline
7
+ curve analysis, material balance). Source: https://github.com/mwburgoyne/pyResToolbox
8
+ (server code in `mcp/`). Contact: Mark Burgoyne.
9
+
10
+ It gives an LLM seven tools: three discovery/dispatch tools that reach the
11
+ library's full public API (about 125 functions), a correlation-method
12
+ recommender, and three one-shot calculation wrappers. Module documentation
13
+ with worked examples is served as MCP resources. Wrong inputs return errors
14
+ that list the valid options, so the model self-corrects without help.
15
+
16
+ ## Security properties
17
+
18
+ - Pure computation. The server makes no outbound network calls, reads no
19
+ external data sources, and writes nothing to disk. Requests contain only
20
+ numeric/string calculation inputs; responses are JSON numbers and text.
21
+ - No credentials, no state, no user data. Nothing is retained between
22
+ requests.
23
+ - Licence: GPL-3.0. Internal use is not distribution, so no obligations are
24
+ triggered, and the source is public in any case.
25
+
26
+ ## How to deploy
27
+
28
+ 1. Build the container from the repo's `mcp/` directory (Dockerfile
29
+ included; pyresToolbox version is pinned via a build arg). No base-image
30
+ requirements beyond python:3.11-slim.
31
+ 2. Run it anywhere inside the Santos network. It listens on port 8000 and
32
+ serves MCP streamable HTTP at `/mcp`. It is stateless: replicas can sit
33
+ behind a load balancer without session affinity.
34
+ 3. Put your standard API gateway in front for authentication. The server
35
+ itself does not authenticate; it expects to live behind the gateway.
36
+ 4. Register the endpoint URL with Mawson as a remote MCP server.
37
+
38
+ If Mawson does not support remote MCP servers and needs an OpenAPI tool
39
+ spec instead, tell us; the tool layer is plain typed Python and a REST
40
+ wrapper is straightforward.
41
+
42
+ ## Sizing
43
+
44
+ Single-digit milliseconds to low seconds per calculation, CPU-bound, modest
45
+ memory (numpy/scipy stack, roughly 500 MB resident). One small container is
46
+ enough to start; scale horizontally if usage grows.
47
+
48
+ ## Updates
49
+
50
+ Version bumps are deliberate: rebuild the image with a new
51
+ `PYRESTOOLBOX_VERSION` build arg after the library publishes a release.
52
+ The calculation library is regression-tested (900+ tests, frozen numerical
53
+ baselines, documentation examples verified in CI).
@@ -0,0 +1,23 @@
1
+ # pyrestoolbox-mcp: remote MCP server (streamable HTTP)
2
+ # Build from the mcp/ directory: docker build -t pyrestoolbox-mcp .
3
+ # Run: docker run -p 8000:8000 pyrestoolbox-mcp
4
+ # MCP endpoint: http://<host>:8000/mcp
5
+
6
+ FROM python:3.11-slim
7
+
8
+ # Pin the library version for reproducible corporate deployments; bump
9
+ # deliberately and rebuild.
10
+ ARG PYRESTOOLBOX_VERSION=3.7.0
11
+
12
+ WORKDIR /app
13
+ COPY . /app/mcp
14
+
15
+ RUN pip install --no-cache-dir "pyrestoolbox==${PYRESTOOLBOX_VERSION}" /app/mcp
16
+
17
+ # Non-root runtime user
18
+ RUN useradd --create-home --shell /usr/sbin/nologin mcp
19
+ USER mcp
20
+
21
+ EXPOSE 8000
22
+
23
+ CMD ["pyrestoolbox-mcp", "--transport", "streamable-http", "--host", "0.0.0.0", "--port", "8000"]
@@ -0,0 +1,87 @@
1
+ # pyrestoolbox-mcp
2
+
3
+ MCP (Model Context Protocol) server exposing the
4
+ [pyrestoolbox](https://github.com/mwburgoyne/pyResToolbox) reservoir
5
+ engineering library to agents without a Python execution environment
6
+ (Claude Desktop, claude.ai connectors, other MCP hosts).
7
+
8
+ If your agent CAN run Python, prefer `pip install pyrestoolbox` directly -
9
+ the library ships its full documentation inside the wheel
10
+ (`pyrestoolbox.docs_dir()`), and native code execution is more flexible than
11
+ any tool surface.
12
+
13
+ ## Design
14
+
15
+ A small meta-tool surface instead of one MCP tool per library function:
16
+
17
+ | Tool | Purpose |
18
+ |---|---|
19
+ | `list_functions(module?)` | Module map, or public functions of one module |
20
+ | `describe(name)` | Signature + docstring for `module.function` |
21
+ | `call(name, arguments)` | Invoke any public function with keyword args |
22
+ | `recommend_methods(...)` | Correlation-method recommendations with rationale |
23
+ | `co2_brine_props(...)` | One-shot CO2-saturated brine properties |
24
+ | `sw_brine_props(...)` | One-shot multicomponent gas-saturated brine properties |
25
+ | `fit_and_forecast(...)` | Fit a decline model and forecast in one call |
26
+
27
+ Every module's RST documentation (shipped in the pyrestoolbox wheel) is
28
+ exposed as an MCP resource: `docs://gas`, `docs://oil`, `docs://index`, ...
29
+
30
+ Correlation methods are passed as strings (`"zmethod": "DAK"`); an invalid
31
+ method string returns an error listing the valid options, so agents
32
+ self-correct in one round trip. Results are JSON: numpy arrays become lists,
33
+ DataFrames become `{columns, records}`, non-finite floats become null.
34
+
35
+ ## Install and configure
36
+
37
+ ```
38
+ pip install pyrestoolbox-mcp # once published; or: pip install ./mcp
39
+ ```
40
+
41
+ Claude Code:
42
+
43
+ ```
44
+ claude mcp add pyrestoolbox -- pyrestoolbox-mcp
45
+ ```
46
+
47
+ Claude Desktop (`claude_desktop_config.json`):
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "pyrestoolbox": {
53
+ "command": "pyrestoolbox-mcp"
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## Remote deployment (enterprise LLM platforms)
60
+
61
+ The server also runs as a remote MCP server over streamable HTTP:
62
+
63
+ ```
64
+ pyrestoolbox-mcp --transport streamable-http --host 0.0.0.0 --port 8000
65
+ ```
66
+
67
+ It is stateless (no session affinity needed behind a load balancer) and
68
+ serves the MCP endpoint at `/mcp`. A `Dockerfile` is included; put an API
69
+ gateway in front for authentication. See `DEPLOYMENT.md` for the platform
70
+ team one-pager.
71
+
72
+ ## Tests
73
+
74
+ ```
75
+ PYTHONPATH=<repo>:<repo>/mcp python3 -m pytest mcp/tests/ -q
76
+ ```
77
+
78
+ ## Notes
79
+
80
+ - Units default to oilfield (psia, deg F, ft, mD, cP); pass `metric: true`
81
+ where the function supports it. The plyasunov module is SI (K, MPa).
82
+ - The oil module is scalar-only; gas and brine functions accept lists.
83
+ - Stateful classes (`GasPVT`, `CO2_Brine_Mixture`, `SoreideWhitson`,
84
+ `DeclineResult`) are not exposed through `call` - the one-shot wrapper
85
+ tools cover the common workflows.
86
+ - File-writing simtools functions keep their `export=False` defaults, so
87
+ calls return table text rather than touching disk.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyrestoolbox-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server exposing the pyrestoolbox reservoir engineering library"
9
+ license = {text = "GPL-3.0-or-later"}
10
+ authors = [{name = "Mark W. Burgoyne", email = "mark.w.burgoyne@gmail.com"}]
11
+ keywords = ["mcp", "petroleum", "reservoir", "pyrestoolbox"]
12
+ requires-python = ">=3.10"
13
+ dependencies = [
14
+ "mcp>=1.2",
15
+ "pyrestoolbox>=3.7",
16
+ ]
17
+ readme = "README.md"
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/mwburgoyne/pyResToolbox"
21
+
22
+ [project.scripts]
23
+ pyrestoolbox-mcp = "pyrestoolbox_mcp.server:main"
24
+
25
+ [tool.setuptools.packages.find]
26
+ include = ["pyrestoolbox_mcp*"]
@@ -0,0 +1,3 @@
1
+ """MCP server exposing the pyrestoolbox reservoir engineering library."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,50 @@
1
+ """Convert pyrestoolbox return values to JSON-serializable structures."""
2
+
3
+ import dataclasses
4
+ import math
5
+ from enum import Enum
6
+
7
+
8
+ def to_jsonable(obj):
9
+ """Recursively convert obj to JSON-serializable Python primitives.
10
+
11
+ numpy scalars/arrays, pandas DataFrames/Series, dataclasses, Enums and
12
+ nested containers are handled. Non-finite floats become None (JSON has
13
+ no NaN/inf).
14
+ """
15
+ if obj is None or isinstance(obj, (bool, str)):
16
+ return obj
17
+ if isinstance(obj, int):
18
+ return int(obj)
19
+ if isinstance(obj, float): # np.float64 subclasses float - cast to plain float
20
+ return float(obj) if math.isfinite(obj) else None
21
+ if isinstance(obj, Enum):
22
+ return obj.name
23
+ if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
24
+ return {f.name: to_jsonable(getattr(obj, f.name)) for f in dataclasses.fields(obj)}
25
+ # numpy
26
+ try:
27
+ import numpy as np
28
+ if isinstance(obj, np.generic):
29
+ return to_jsonable(obj.item())
30
+ if isinstance(obj, np.ndarray):
31
+ return [to_jsonable(v) for v in obj.tolist()]
32
+ except ImportError:
33
+ pass
34
+ # pandas
35
+ try:
36
+ import pandas as pd
37
+ if isinstance(obj, pd.DataFrame):
38
+ return {
39
+ "columns": [str(c) for c in obj.columns],
40
+ "records": to_jsonable(obj.to_dict(orient="records")),
41
+ }
42
+ if isinstance(obj, pd.Series):
43
+ return to_jsonable(obj.tolist())
44
+ except ImportError:
45
+ pass
46
+ if isinstance(obj, dict):
47
+ return {str(k): to_jsonable(v) for k, v in obj.items()}
48
+ if isinstance(obj, (list, tuple, set)):
49
+ return [to_jsonable(v) for v in obj]
50
+ return str(obj)
@@ -0,0 +1,295 @@
1
+ """MCP server exposing the pyrestoolbox reservoir engineering library.
2
+
3
+ Meta-tool design: rather than one MCP tool per library function (125+), a
4
+ small set of tools lets an agent discover and call the whole public API:
5
+
6
+ list_functions - module map, or the public functions of one module
7
+ describe - signature + docstring for module.function
8
+ call - invoke any public function with keyword arguments
9
+ recommend_methods - structured correlation-method recommendations
10
+ co2_brine_props / sw_brine_props / fit_and_forecast - one-shot wrappers
11
+ around the stateful classes
12
+
13
+ The full RST documentation shipped inside the pyrestoolbox wheel is exposed
14
+ as MCP resources (docs://gas, docs://oil, ...).
15
+ """
16
+
17
+ import importlib
18
+ import inspect
19
+
20
+ from mcp.server.fastmcp import FastMCP
21
+
22
+ from pyrestoolbox_mcp.serialize import to_jsonable
23
+
24
+ # Modules whose public functions are callable through this server.
25
+ ALLOWED_MODULES = {
26
+ "gas": "Gas PVT (Z-factor, viscosity, Bg, Cg, density, pseudopressure), flow rates, pseudoskins, hydrates",
27
+ "oil": "Oil PVT (Pb, Rs, Bo, density, viscosity, compressibility), flow rates, harmonization (scalar inputs only)",
28
+ "brine": "Brine properties: CH4-saturated, CO2-saturated, multicomponent gas-saturated",
29
+ "plyasunov": "Infinite-dilution partial molar volumes of dissolved gases in water (SI units: K, MPa, cm3/mol)",
30
+ "nodal": "VLP/IPR nodal analysis: fbhp, fthp, outflow_curve, ipr_curve, operating_point",
31
+ "dca": "Decline curve analysis: Arps, Duong, modified hyperbolic, two-segment hyperbolic, fitting, EUR",
32
+ "matbal": "Material balance: P/Z gas and Havlena-Odeh oil",
33
+ "simtools": "Simulation helpers: rel-perm tables, aquifer influence, VFP/PVT tables, deck checking",
34
+ "layer": "Lorenz heterogeneity and permeability layering",
35
+ "library": "Component critical property database",
36
+ "recommend": "Correlation method recommendation engine",
37
+ "sensitivity": "Parameter sweeps and tornado analysis",
38
+ }
39
+
40
+ mcp = FastMCP(
41
+ "pyrestoolbox",
42
+ instructions=(
43
+ "Petroleum/reservoir engineering calculations from the pyrestoolbox "
44
+ "library. Start with list_functions() for the module map, "
45
+ "describe('module.function') before calling anything, and "
46
+ "recommend_methods(...) if unsure which correlation method suits a "
47
+ "fluid. All inputs default to oilfield units (psia, deg F, ft, mD, "
48
+ "cP); pass metric=True for Eclipse METRIC units where supported. "
49
+ "Full module documentation is available as docs:// resources."
50
+ ),
51
+ )
52
+
53
+
54
+ def _get_module(module: str):
55
+ if module not in ALLOWED_MODULES:
56
+ raise ValueError(f"Unknown module: {module!r}. Available: {sorted(ALLOWED_MODULES)}")
57
+ return importlib.import_module(f"pyrestoolbox.{module}")
58
+
59
+
60
+ def _public_names(mod):
61
+ names = getattr(mod, "__all__", None)
62
+ if names is None:
63
+ names = [n for n in dir(mod) if not n.startswith("_")]
64
+ return [n for n in names if not n.startswith("_")]
65
+
66
+
67
+ def _resolve(name: str):
68
+ """Resolve 'module.function' to (module, attribute)."""
69
+ if "." not in name:
70
+ raise ValueError(
71
+ f"Use 'module.function' form, e.g. 'gas.gas_z'. Got: {name!r}. "
72
+ f"Modules: {sorted(ALLOWED_MODULES)}"
73
+ )
74
+ module, _, attr = name.partition(".")
75
+ mod = _get_module(module)
76
+ if attr.startswith("_") or attr not in _public_names(mod):
77
+ raise ValueError(f"{attr!r} is not a public name in pyrestoolbox.{module}")
78
+ return mod, getattr(mod, attr)
79
+
80
+
81
+ @mcp.tool()
82
+ def list_functions(module: str = "") -> dict:
83
+ """List pyrestoolbox modules, or the public functions of one module.
84
+
85
+ With no argument, returns {module: description}. With a module name
86
+ (e.g. 'gas'), returns {function: one-line summary} for every public
87
+ function and class in that module.
88
+ """
89
+ if not module:
90
+ return dict(ALLOWED_MODULES)
91
+ mod = _get_module(module)
92
+ out = {}
93
+ for name in _public_names(mod):
94
+ obj = getattr(mod, name, None)
95
+ if callable(obj):
96
+ doc = inspect.getdoc(obj) or ""
97
+ out[name] = doc.split("\n", 1)[0].strip()
98
+ return out
99
+
100
+
101
+ @mcp.tool()
102
+ def describe(name: str) -> str:
103
+ """Signature and full docstring for a public function or class.
104
+
105
+ name is 'module.function', e.g. 'gas.gas_z' or 'dca.fit_decline'.
106
+ Also see the docs://<module> resources for worked examples.
107
+ """
108
+ _, obj = _resolve(name)
109
+ target = obj.__init__ if inspect.isclass(obj) else obj
110
+ try:
111
+ raw = inspect.signature(target)
112
+ # Strip type annotations - docstrings carry the units/types, and
113
+ # numpy typing unions are unreadable noise in a tool result.
114
+ params = [p.replace(annotation=inspect.Parameter.empty)
115
+ for p in raw.parameters.values() if p.name != "self"]
116
+ sig = str(raw.replace(parameters=params,
117
+ return_annotation=inspect.Signature.empty))
118
+ except (TypeError, ValueError):
119
+ sig = "(...)"
120
+ kind = "class" if inspect.isclass(obj) else "function"
121
+ doc = inspect.getdoc(obj) or "(no docstring)"
122
+ return f"{kind} {name}{sig}\n\n{doc}"
123
+
124
+
125
+ @mcp.tool()
126
+ def call(name: str, arguments: dict = {}) -> dict:
127
+ """Call any public pyrestoolbox function with keyword arguments.
128
+
129
+ name is 'module.function', e.g. 'gas.gas_z'. arguments is a JSON object
130
+ of keyword arguments, e.g. {"p": 2000, "sg": 0.75, "degf": 200}.
131
+ Correlation methods are strings (e.g. "zmethod": "DAK"); an invalid
132
+ method string returns an error listing the valid options. Gas and brine
133
+ functions accept lists for pressure-like inputs; oil functions are
134
+ scalar-only. Classes cannot be called here - use the one-shot wrapper
135
+ tools (co2_brine_props, sw_brine_props, fit_and_forecast) instead.
136
+ """
137
+ _, obj = _resolve(name)
138
+ if inspect.isclass(obj):
139
+ raise ValueError(
140
+ f"{name} is a class. Use describe({name!r}) to inspect it, or a "
141
+ "one-shot wrapper tool (co2_brine_props, sw_brine_props, "
142
+ "fit_and_forecast) if one covers it."
143
+ )
144
+ if not callable(obj):
145
+ raise ValueError(f"{name} is not callable (it is {type(obj).__name__})")
146
+ result = obj(**arguments)
147
+ return {"result": to_jsonable(result)}
148
+
149
+
150
+ @mcp.tool()
151
+ def recommend_methods(sg: float = 0.65, co2: float = 0, h2s: float = 0,
152
+ n2: float = 0, h2: float = 0, api: float | None = None,
153
+ deviation: float = 0, well_type: str = "gas") -> dict:
154
+ """Recommend correlation methods for a fluid/well, with rationale.
155
+
156
+ Composition fractions are molar. api: stock tank oil gravity (include to
157
+ get oil PVT method recommendations). deviation: max wellbore deviation
158
+ from vertical (degrees). well_type: 'gas' or 'oil'. Returns one
159
+ recommendation per category (zmethod, cmethod, vlp_method, ...), each
160
+ with the recommended method, rationale, alternatives and whether the
161
+ choice is mandatory.
162
+ """
163
+ from pyrestoolbox import recommend
164
+ recs = recommend.recommend_methods(sg=sg, co2=co2, h2s=h2s, n2=n2, h2=h2,
165
+ api=api, deviation=deviation,
166
+ well_type=well_type)
167
+ return to_jsonable(recs)
168
+
169
+
170
+ @mcp.tool()
171
+ def co2_brine_props(pres: float, temp: float, ppm: float = 0,
172
+ cw_sat: bool = False, metric: bool = False) -> dict:
173
+ """CO2-saturated brine properties (Spycher-Pruess mutual solubility).
174
+
175
+ pres: pressure (psia, or barsa if metric=True). temp: temperature
176
+ (deg F, or deg C if metric=True). ppm: NaCl-equivalent salinity (weight
177
+ salt per million weight of brine). cw_sat=True also computes the
178
+ saturated pseudo-compressibility. Returns aqueous/vapor compositions,
179
+ gas density (g/cm3), brine density/viscosity/Bw triplets
180
+ [CO2-saturated, pure brine, freshwater], Rs and compressibilities.
181
+ """
182
+ from pyrestoolbox import brine
183
+ mix = brine.CO2_Brine_Mixture(pres=pres, temp=temp, ppm=ppm,
184
+ cw_sat=cw_sat, metric=metric)
185
+ keys = ("x", "y", "xSalt", "rhoGas", "bDen", "bVis", "bVisblty", "bw",
186
+ "Rs", "Cf_usat", "Cf_sat")
187
+ return {k: to_jsonable(getattr(mix, k, None)) for k in keys}
188
+
189
+
190
+ @mcp.tool()
191
+ def sw_brine_props(pres: float, temp: float, ppm: float = 0,
192
+ y_CO2: float = 0, y_H2S: float = 0, y_N2: float = 0,
193
+ y_H2: float = 0, sg: float = 0.65, cw_sat: bool = False,
194
+ metric: bool = False) -> dict:
195
+ """Multicomponent gas-saturated brine properties (Soreide-Whitson VLE).
196
+
197
+ pres: pressure (psia, or barsa if metric=True). temp: temperature
198
+ (deg F, or deg C if metric=True). ppm: NaCl-equivalent salinity.
199
+ y_*: molar fractions of CO2/H2S/N2/H2 in the equilibrium gas; the
200
+ remainder is hydrocarbon gas of specific gravity sg. Returns aqueous
201
+ composition, brine density/viscosity triplets, Rs, water content and
202
+ compressibilities.
203
+ """
204
+ from pyrestoolbox import brine
205
+ sw = brine.SoreideWhitson(pres=pres, temp=temp, ppm=ppm, y_CO2=y_CO2,
206
+ y_H2S=y_H2S, y_N2=y_N2, y_H2=y_H2, sg=sg,
207
+ cw_sat=cw_sat, metric=metric)
208
+ keys = ("x", "bDen", "bVis", "bw", "Rs", "Rs_total", "water_content",
209
+ "Cf_usat", "Cf_sat")
210
+ return {k: to_jsonable(getattr(sw, k, None)) for k in keys}
211
+
212
+
213
+ @mcp.tool()
214
+ def fit_and_forecast(t: list, q: list, t_end: float, method: str = "best",
215
+ dt: float = 1.0, q_min: float = 0.0) -> dict:
216
+ """Fit a decline model to rate-time data and forecast to t_end.
217
+
218
+ t: time array. q: rate array (same length; any consistent units - the
219
+ dca module is unit-agnostic). method: 'best' (excludes 'mh'/'hyp2'),
220
+ 'exponential', 'harmonic', 'hyperbolic', 'duong', 'mh' or 'hyp2'.
221
+ Forecast runs from the fitted model at dt spacing until t_end or until
222
+ rate falls to q_min. Returns fitted parameters (qi, di, b, ...),
223
+ r_squared, EUR and the forecast t/q/cumulative arrays.
224
+ """
225
+ from pyrestoolbox import dca
226
+ fit = dca.fit_decline(t=t, q=q, method=method)
227
+ fc = dca.forecast(fit, t_end=t_end, dt=dt, q_min=q_min)
228
+ return {
229
+ "fit": {k: to_jsonable(getattr(fit, k)) for k in
230
+ ("method", "qi", "di", "b", "a", "m", "dterm", "b2", "telf",
231
+ "r_squared")},
232
+ "forecast": {"t": to_jsonable(fc.t), "q": to_jsonable(fc.q),
233
+ "cum": to_jsonable(fc.Qcum), "eur": to_jsonable(fc.eur)},
234
+ }
235
+
236
+
237
+ def _register_doc_resources():
238
+ """Expose every RST file shipped in the pyrestoolbox wheel as docs://<name>."""
239
+ import os
240
+ import pyrestoolbox
241
+ docs = pyrestoolbox.docs_dir()
242
+ for fname in sorted(os.listdir(docs)):
243
+ if not fname.endswith(".rst"):
244
+ continue
245
+ stem = fname[:-4]
246
+ path = os.path.join(docs, fname)
247
+
248
+ def _make_reader(p):
249
+ def _read() -> str:
250
+ with open(p, encoding="utf-8") as f:
251
+ return f.read()
252
+ return _read
253
+
254
+ mcp.resource(
255
+ f"docs://{stem}",
256
+ name=f"pyrestoolbox {stem} documentation",
257
+ description=f"RST documentation with worked examples: {stem}",
258
+ mime_type="text/x-rst",
259
+ )(_make_reader(path))
260
+
261
+
262
+ _register_doc_resources()
263
+
264
+
265
+ def build_parser():
266
+ import argparse
267
+ parser = argparse.ArgumentParser(
268
+ prog="pyrestoolbox-mcp",
269
+ description="MCP server exposing the pyrestoolbox library",
270
+ )
271
+ parser.add_argument("--transport", choices=["stdio", "streamable-http"],
272
+ default="stdio",
273
+ help="stdio for local hosts (default); streamable-http "
274
+ "for remote deployment (serves on /mcp)")
275
+ parser.add_argument("--host", default="127.0.0.1",
276
+ help="bind address for streamable-http (default "
277
+ "127.0.0.1; use 0.0.0.0 in a container)")
278
+ parser.add_argument("--port", type=int, default=8000,
279
+ help="port for streamable-http (default 8000)")
280
+ return parser
281
+
282
+
283
+ def main():
284
+ args = build_parser().parse_args()
285
+ if args.transport == "streamable-http":
286
+ mcp.settings.host = args.host
287
+ mcp.settings.port = args.port
288
+ # Independent request handling; no server-side session affinity needed
289
+ # behind a load balancer.
290
+ mcp.settings.stateless_http = True
291
+ mcp.run(transport=args.transport)
292
+
293
+
294
+ if __name__ == "__main__":
295
+ main()