mock-gkn-hpc 0.3.0__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.
@@ -0,0 +1,53 @@
1
+ name: Test and Publish
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ release:
9
+ types: [published]
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ python-version: ["3.14"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v4
23
+
24
+ - name: Set up Python ${{ matrix.python-version }}
25
+ run: uv python install ${{ matrix.python-version }}
26
+
27
+ - name: Install dependencies
28
+ run: uv sync
29
+
30
+ - name: Smoke import
31
+ run: uv run python -c "import mock_gkn_hpc; print(mock_gkn_hpc.__version__)"
32
+
33
+ publish:
34
+ needs: test
35
+ runs-on: ubuntu-latest
36
+ if: github.event_name == 'release'
37
+ permissions:
38
+ id-token: write
39
+
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+
43
+ - name: Install uv
44
+ uses: astral-sh/setup-uv@v4
45
+
46
+ - name: Set up Python
47
+ run: uv python install 3.14
48
+
49
+ - name: Build package
50
+ run: uv build
51
+
52
+ - name: Publish to PyPI
53
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Mock HPC job artifacts
13
+ runs/
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "mock-gkn-hpc": {
4
+ "type": "stdio",
5
+ "command": "uv",
6
+ "args": ["run", "mock-gkn-hpc"]
7
+ }
8
+ }
9
+ }
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,42 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Purpose
6
+
7
+ A FastMCP server that **mocks** GKN's HPC for Ansys / Finite Element analysis. It is an external demo that lets clients exercise the full submit → poll → fetch-artifacts loop against a fake HPC, with no real solver, queue, or cluster behind it.
8
+
9
+ ## Architecture
10
+
11
+ Packaged distribution under `src/mock_gkn_hpc/`. Single module `server.py` (~190 lines of FastMCP). The model is intentionally tiny:
12
+
13
+ - **Filesystem is the state store.** No in-memory registry. `RUNS_DIR/<job_id>/` exists ⇒ job submitted; `results.rst` present ⇒ ready; older than `JOB_TTL_SECONDS` ⇒ expired. `_job_status()` derives status from directory + `results.rst` mtime — `log.txt` is *not* a readiness sentinel because it's created at submit time and grown during the run.
14
+ - **`RUNS_DIR` defaults to `gettempdir()/mock-gkn-hpc`.** Overridable via `--runs-dir` CLI flag or `MOCK_GKN_HPC_RUNS_DIR` env var. Set in `main()` before `mcp.run()` — the module-level `RUNS_DIR` is reassigned, so all tools/resources see the override.
15
+ - **Async submit, streaming log, terminal results.** `submit_ansys_run` returns immediately with a `job_id` and two `ansys://...` URIs. A background `asyncio.create_task(_finish(job_id))` walks `LOG_SCHEDULE` (a list of `(sleep, line)` tuples summing to `MOCK_RUN_SECONDS = 30s`) appending each line to `log.txt`, then renders `results.rst` from `src/mock_gkn_hpc/templates/results.rst.tmpl` with current date/time/job_id substituted in.
16
+ - **Resources are templated**: `ansys://{job_id}/log` and `ansys://{job_id}/results`. The log resource serves whatever has been written so far — clients can poll it to watch progress mid-run. The results resource 404s ("not found or still running") until `_finish` writes it at t≈30s. Both self-delete + 404 when past TTL.
17
+ - **`age_seconds` in `list_jobs` is intentionally inconsistent**: time-since-submit while pending (dir mtime), time-since-completion once ready (`results.rst` mtime). Not a bug — derives from the dir-vs-file state model.
18
+ - `submit_ansys_run`'s `input_file` and `version` are part of the tool schema for client realism but unused by the mock.
19
+
20
+ The `templates/` directory ships *inside* the wheel (`hatchling` includes everything under `src/mock_gkn_hpc/`), so `Path(__file__).parent / "templates"` resolves identically in editable and installed mode.
21
+
22
+ ## Dev workflow
23
+
24
+ The server is registered via project-scoped `.mcp.json` as a stdio server invoking `uv run mock-gkn-hpc` (the entry-point script that `uv sync` installs in editable mode). Claude Code spawns it on connect; **after every edit to `server.py`, manually reconnect via `/mcp` in Claude Code** to pick up the change.
25
+
26
+ Avoid `fastmcp call ... submit_ansys_run`: each call is a one-shot subprocess that exits before the 30s `_finish()` background task fires, leaving zombie pending jobs in `RUNS_DIR`. Either go through Claude Code (long-lived stdio session) or write a client that holds the connection open past `MOCK_RUN_SECONDS`.
27
+
28
+ Useful commands:
29
+
30
+ ```bash
31
+ uv sync # editable install
32
+ uv run mock-gkn-hpc --help # show CLI flags
33
+ uv run mock-gkn-hpc # run the server over stdio
34
+ uv build # produce wheel + sdist in dist/
35
+ uvx --from ./dist/mock_gkn_hpc-*.whl mock-gkn-hpc --help # smoke-test the built wheel
36
+ ```
37
+
38
+ No tests, no linter configured.
39
+
40
+ ## Gotchas
41
+
42
+ - Python ≥ 3.14. Per global guidance, use 3.14 syntax — no `from typing import Optional/List/Dict`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alejandro Pradas Gómez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: mock-gkn-hpc
3
+ Version: 0.3.0
4
+ Summary: Public MCP mock of GKN's internal HPC server for Ansys/FEA analysis
5
+ Project-URL: Homepage, https://github.com/alex-pradas/mock-gkn-hpc
6
+ Project-URL: Repository, https://github.com/alex-pradas/mock-gkn-hpc
7
+ Author-email: Alejandro Pradas Gómez <alex@pradas.eu>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: ansys,fastmcp,fea,gkn,hpc,mcp
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.14
17
+ Requires-Dist: fastmcp>=3.2.4
18
+ Description-Content-Type: text/markdown
19
+
20
+ # mock-gkn-hpc
21
+
22
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.18836517.svg)](https://doi.org/10.5281/zenodo.18836517)
23
+
24
+ A public MCP **mock** of GKN's internal HPC server for Ansys / Finite Element analysis. It lets external clients exercise the full submit → poll → fetch-artifacts loop against a fake HPC — no real solver, queue, or cluster behind it.
25
+
26
+ This is a sample MCP server developed alongside the DUCTILE agentic orchestration paper. See the [DUCTILE repository](https://github.com/alex-pradas/DUCTILE) or the paper (DOI: TBD) for context.
27
+
28
+ ## Install and run
29
+
30
+ ```bash
31
+ uvx mock-gkn-hpc
32
+ ```
33
+
34
+ Or wire it into your MCP client config:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "mock-gkn-hpc": {
40
+ "command": "uvx",
41
+ "args": ["mock-gkn-hpc"]
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ Job artifacts default to a system tempdir (`gettempdir()/mock-gkn-hpc`). Override with `--runs-dir /custom/path` or the `MOCK_GKN_HPC_RUNS_DIR` env var.
@@ -0,0 +1,28 @@
1
+ # mock-gkn-hpc
2
+
3
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.18836517.svg)](https://doi.org/10.5281/zenodo.18836517)
4
+
5
+ A public MCP **mock** of GKN's internal HPC server for Ansys / Finite Element analysis. It lets external clients exercise the full submit → poll → fetch-artifacts loop against a fake HPC — no real solver, queue, or cluster behind it.
6
+
7
+ This is a sample MCP server developed alongside the DUCTILE agentic orchestration paper. See the [DUCTILE repository](https://github.com/alex-pradas/DUCTILE) or the paper (DOI: TBD) for context.
8
+
9
+ ## Install and run
10
+
11
+ ```bash
12
+ uvx mock-gkn-hpc
13
+ ```
14
+
15
+ Or wire it into your MCP client config:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "mock-gkn-hpc": {
21
+ "command": "uvx",
22
+ "args": ["mock-gkn-hpc"]
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ Job artifacts default to a system tempdir (`gettempdir()/mock-gkn-hpc`). Override with `--runs-dir /custom/path` or the `MOCK_GKN_HPC_RUNS_DIR` env var.
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "mock-gkn-hpc"
3
+ version = "0.3.0"
4
+ description = "Public MCP mock of GKN's internal HPC server for Ansys/FEA analysis"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Alejandro Pradas Gómez", email = "alex@pradas.eu" }]
9
+ keywords = ["mcp", "ansys", "fea", "hpc", "fastmcp", "gkn"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.14",
16
+ ]
17
+ dependencies = [
18
+ "fastmcp>=3.2.4",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/alex-pradas/mock-gkn-hpc"
23
+ Repository = "https://github.com/alex-pradas/mock-gkn-hpc"
24
+
25
+ [project.scripts]
26
+ mock-gkn-hpc = "mock_gkn_hpc.server:main"
27
+
28
+ [build-system]
29
+ requires = ["hatchling"]
30
+ build-backend = "hatchling.build"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/mock_gkn_hpc"]
@@ -0,0 +1,4 @@
1
+ """Public MCP mock of GKN's internal HPC for Ansys / FEA analysis."""
2
+ from importlib.metadata import version
3
+
4
+ __version__ = version("mock-gkn-hpc")
File without changes
@@ -0,0 +1,186 @@
1
+ import argparse
2
+ import asyncio
3
+ import os
4
+ import shutil
5
+ import tempfile
6
+ import time
7
+ import uuid
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ from fastmcp import FastMCP
12
+ from pydantic import FilePath
13
+
14
+ mcp = FastMCP(
15
+ "HPC at GKN, external demo",
16
+ instructions="Simulates the HPC for Finite Elements analyisis at GKN",
17
+ )
18
+
19
+ DEFAULT_RUNS_DIR = Path(tempfile.gettempdir()) / "mock-gkn-hpc"
20
+ RUNS_DIR = DEFAULT_RUNS_DIR
21
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
22
+ JOB_TTL_SECONDS = 3600
23
+ MOCK_RUN_SECONDS = 30
24
+
25
+ # (sleep_before_writing_seconds, line). Sleeps sum to MOCK_RUN_SECONDS.
26
+ LOG_SCHEDULE: list[tuple[float, str]] = [
27
+ (0.0, "ANSYS Mechanical 2025R1 starting..."),
28
+ (0.2, "License: ANSYS Mechanical Enterprise -- checked out"),
29
+ (0.3, "Reading input file: model.cdb"),
30
+ (0.5, "Mesh statistics: 12847 nodes, 8432 elements, 38541 dofs"),
31
+ (0.5, "Material: Structural Steel (E=200 GPa, nu=0.30)"),
32
+ (0.5, "Boundary conditions: 1 fixed support, 2 force loads"),
33
+ (0.5, ""),
34
+ (0.0, "--- SOLUTION ---"),
35
+ (0.5, "Solver: Sparse Direct"),
36
+ (0.5, "Reordering equations (METIS)..."),
37
+ (1.0, "Equation reordering complete; matrix non-zeros = 1.84M, factor non-zeros = 8.2M"),
38
+ (1.0, "Symbolic factorization..."),
39
+ (2.0, "Numeric factorization (LDL^T)..."),
40
+ (5.0, "Factor complete; peak memory = 412 MB"),
41
+ (2.0, "Forward/back substitution..."),
42
+ (4.0, "Solver complete; max residual = 4.21e-09"),
43
+ (2.0, "*** LOAD STEP 1 SUBSTEP 1 COMPLETED. CUM ITER = 1 ***"),
44
+ (2.0, ""),
45
+ (0.0, "--- POSTPROCESSING ---"),
46
+ (1.0, "Computing element results..."),
47
+ (2.0, "Max von Mises stress: 187.4 MPa @ node 4521"),
48
+ (1.0, "Max displacement: 0.842 mm @ node 9183"),
49
+ (1.0, "Reaction force at fixed support: (-2104.3, 8.2, -45.1) N"),
50
+ (1.0, "Writing results database..."),
51
+ (1.5, "Wrote results.rst"),
52
+ (0.5, "Solution complete. CPU time: 28.7s, elapsed: 30.1s"),
53
+ ]
54
+
55
+
56
+ def _render_results(job_id: str, dst: Path) -> None:
57
+ tmpl = (TEMPLATES_DIR / "results.rst.tmpl").read_text()
58
+ now = datetime.now()
59
+ dst.write_text(tmpl.format(
60
+ run_date=now.strftime("%Y-%m-%d"),
61
+ run_time=now.strftime("%H:%M:%S"),
62
+ job_id=job_id,
63
+ ))
64
+
65
+
66
+ async def _finish(job_id: str) -> None:
67
+ job_dir = RUNS_DIR / job_id
68
+ job_dir.mkdir(parents=True, exist_ok=True)
69
+ log = job_dir / "log.txt"
70
+ log.write_text("")
71
+ for delay, line in LOG_SCHEDULE:
72
+ if delay:
73
+ await asyncio.sleep(delay)
74
+ with log.open("a") as f:
75
+ f.write(line + "\n")
76
+ _render_results(job_id, job_dir / "results.rst")
77
+
78
+
79
+ def _job_status(job_dir: Path) -> tuple[str, float]:
80
+ results = job_dir / "results.rst"
81
+ if not results.exists():
82
+ return "pending", time.time() - job_dir.stat().st_mtime
83
+ age = time.time() - results.stat().st_mtime
84
+ if age > JOB_TTL_SECONDS:
85
+ return "expired", age
86
+ return "ready", age
87
+
88
+
89
+ @mcp.tool
90
+ async def submit_ansys_run(
91
+ input_file: FilePath,
92
+ version: str = "2025r1",
93
+ ) -> dict:
94
+ """Submits run to HPC. Returns immediately; log and results become
95
+ available at the returned URIs after ~30s. Resources expire after 1h."""
96
+ job_id = uuid.uuid4().hex[:8]
97
+ (RUNS_DIR / job_id).mkdir(parents=True, exist_ok=True)
98
+ asyncio.create_task(_finish(job_id))
99
+ return {
100
+ "status": "submitted",
101
+ "job_id": job_id,
102
+ "log_uri": f"ansys://{job_id}/log",
103
+ "results_uri": f"ansys://{job_id}/results",
104
+ }
105
+
106
+
107
+ @mcp.tool
108
+ def status() -> str:
109
+ return "online"
110
+
111
+
112
+ @mcp.tool
113
+ def list_jobs() -> list[dict]:
114
+ """Lists every submitted job with its current status (pending/ready/expired) and age in seconds."""
115
+ if not RUNS_DIR.exists():
116
+ return []
117
+ jobs = []
118
+ for job_dir in sorted(RUNS_DIR.iterdir()):
119
+ if not job_dir.is_dir():
120
+ continue
121
+ st, age = _job_status(job_dir)
122
+ jobs.append({
123
+ "job_id": job_dir.name,
124
+ "status": st,
125
+ "age_seconds": round(age, 1),
126
+ "log_uri": f"ansys://{job_dir.name}/log",
127
+ "results_uri": f"ansys://{job_dir.name}/results",
128
+ })
129
+ return jobs
130
+
131
+
132
+ @mcp.tool
133
+ def delete_results() -> dict:
134
+ """Deletes every job's log and results files."""
135
+ if not RUNS_DIR.exists():
136
+ return {"deleted": 0}
137
+ count = sum(1 for p in RUNS_DIR.iterdir() if p.is_dir())
138
+ shutil.rmtree(RUNS_DIR)
139
+ return {"deleted": count}
140
+
141
+
142
+ def _read_artifact(job_id: str, name: str) -> str:
143
+ path = RUNS_DIR / job_id / name
144
+ if not path.exists():
145
+ raise ValueError(f"Job {job_id} {name}: not found or still running.")
146
+ if time.time() - path.stat().st_mtime > JOB_TTL_SECONDS:
147
+ path.unlink(missing_ok=True)
148
+ raise ValueError(f"Job {job_id} {name}: expired.")
149
+ return path.read_text()
150
+
151
+
152
+ @mcp.resource("ansys://{job_id}/log")
153
+ def ansys_log(job_id: str) -> str:
154
+ return _read_artifact(job_id, "log.txt")
155
+
156
+
157
+ @mcp.resource("ansys://{job_id}/results")
158
+ def ansys_results(job_id: str) -> str:
159
+ return _read_artifact(job_id, "results.rst")
160
+
161
+
162
+ def main() -> None:
163
+ parser = argparse.ArgumentParser(
164
+ description="Public MCP mock of GKN's HPC for Ansys/FEA analysis"
165
+ )
166
+ parser.add_argument(
167
+ "--runs-dir",
168
+ type=Path,
169
+ default=None,
170
+ help=f"Where to store job artifacts (default: {DEFAULT_RUNS_DIR})",
171
+ )
172
+ args = parser.parse_args()
173
+
174
+ runs = args.runs_dir
175
+ if runs is None:
176
+ env = os.environ.get("MOCK_GKN_HPC_RUNS_DIR")
177
+ runs = Path(env) if env else DEFAULT_RUNS_DIR
178
+
179
+ global RUNS_DIR
180
+ RUNS_DIR = runs
181
+
182
+ mcp.run()
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -0,0 +1,28 @@
1
+ ANSYS RESULTS FILE (.rst) - text mock
2
+ =====================================
3
+ Run date: {run_date}
4
+ Run time: {run_time}
5
+ Job ID: {job_id}
6
+ Solver: Sparse Direct
7
+ Analysis: Linear Static
8
+
9
+ Summary
10
+ -------
11
+ Nodes: 12847
12
+ Elements: 8432
13
+ DOF: 38541
14
+ Load steps: 1
15
+ Substeps: 1
16
+
17
+ Maximum results
18
+ ---------------
19
+ von Mises stress: 187.4 MPa (node 4521)
20
+ Displacement: 0.842 mm (node 9183)
21
+ Reaction force: (-2104.3, 8.2, -45.1) N
22
+
23
+ Solver
24
+ ------
25
+ Iterations: 1
26
+ Final residual: 4.21e-09
27
+ CPU time: 28.7 s
28
+ Elapsed time: 30.1 s