pytyr-mcp 0.0.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.
- pytyr_mcp-0.0.1/.github/workflows/ci_pytest.yml +43 -0
- pytyr_mcp-0.0.1/.github/workflows/ci_release.yml +33 -0
- pytyr_mcp-0.0.1/.github/workflows/release.yml +80 -0
- pytyr_mcp-0.0.1/.gitignore +27 -0
- pytyr_mcp-0.0.1/PKG-INFO +31 -0
- pytyr_mcp-0.0.1/README.md +18 -0
- pytyr_mcp-0.0.1/pyproject.toml +38 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/__init__.py +5 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/artifacts.py +184 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/config.py +26 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/invoke.py +110 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/paths.py +21 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/planning/__init__.py +1 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/planning/sample_generator/__init__.py +0 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/planning/sample_generator/results.py +146 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/planning/sample_generator/service.py +187 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/planning/sample_generator/tools.py +45 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/planning/solvability/__init__.py +1 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/planning/solvability/schemas.py +14 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/planning/solvability/service.py +117 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/planning/solvability/tools.py +37 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/roles.py +41 -0
- pytyr_mcp-0.0.1/src/pytyr_mcp/server.py +52 -0
- pytyr_mcp-0.0.1/tests/__init__.py +1 -0
- pytyr_mcp-0.0.1/tests/planning/__init__.py +1 -0
- pytyr_mcp-0.0.1/tests/planning/solvability/__init__.py +1 -0
- pytyr_mcp-0.0.1/tests/planning/solvability/test_prove_solvability_output.py +124 -0
- pytyr_mcp-0.0.1/tests/planning/test_sample_generator_output.py +214 -0
- pytyr_mcp-0.0.1/tests/test_packaging.py +56 -0
- pytyr_mcp-0.0.1/tests/test_roles.py +91 -0
- pytyr_mcp-0.0.1/tests/test_server_output_paths.py +82 -0
- pytyr_mcp-0.0.1/uv.lock +1312 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: CI Pytest
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches-ignore:
|
|
6
|
+
- test-pypi
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build-and-test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest]
|
|
16
|
+
python-version: ["3.13"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout repository
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Setup Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install package
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --no-cache-dir --upgrade pip
|
|
30
|
+
python -m pip install --no-cache-dir --verbose .[dev]
|
|
31
|
+
env:
|
|
32
|
+
PIP_NO_CACHE_DIR: 1
|
|
33
|
+
|
|
34
|
+
- name: Import package
|
|
35
|
+
run: python -c "import pytyr_mcp; print(pytyr_mcp.__version__)"
|
|
36
|
+
|
|
37
|
+
- name: Test
|
|
38
|
+
run: |
|
|
39
|
+
if [ -d tests ]; then
|
|
40
|
+
pytest tests
|
|
41
|
+
else
|
|
42
|
+
echo "No tests directory yet; import check completed."
|
|
43
|
+
fi
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- dev
|
|
8
|
+
pull_request:
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build_distributions:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout repository
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.13"
|
|
23
|
+
|
|
24
|
+
- name: Install build frontend
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --no-cache-dir --upgrade pip
|
|
27
|
+
python -m pip install --no-cache-dir build twine
|
|
28
|
+
|
|
29
|
+
- name: Build distributions
|
|
30
|
+
run: python -m build
|
|
31
|
+
|
|
32
|
+
- name: Check distributions
|
|
33
|
+
run: python -m twine check dist/*
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- "v*"
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build_distributions:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout repository
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.13"
|
|
21
|
+
|
|
22
|
+
- name: Install build frontend
|
|
23
|
+
run: |
|
|
24
|
+
python -m pip install --no-cache-dir --upgrade pip
|
|
25
|
+
python -m pip install --no-cache-dir build twine
|
|
26
|
+
|
|
27
|
+
- name: Build distributions
|
|
28
|
+
run: python -m build
|
|
29
|
+
|
|
30
|
+
- name: Check distributions
|
|
31
|
+
run: python -m twine check dist/*
|
|
32
|
+
|
|
33
|
+
- name: Upload distributions artifact
|
|
34
|
+
uses: actions/upload-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: distributions
|
|
37
|
+
path: dist/*
|
|
38
|
+
|
|
39
|
+
publish:
|
|
40
|
+
needs:
|
|
41
|
+
- build_distributions
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
|
|
44
|
+
if: github.repository_owner == 'planning-and-learning'
|
|
45
|
+
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write
|
|
48
|
+
|
|
49
|
+
steps:
|
|
50
|
+
- name: Checkout repository
|
|
51
|
+
uses: actions/checkout@v4
|
|
52
|
+
|
|
53
|
+
- name: Check version matches tag
|
|
54
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
55
|
+
run: |
|
|
56
|
+
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
57
|
+
TAG=${GITHUB_REF#refs/tags/v}
|
|
58
|
+
|
|
59
|
+
echo "Package version: $VERSION"
|
|
60
|
+
echo "Git tag: $TAG"
|
|
61
|
+
|
|
62
|
+
if [ "$VERSION" != "$TAG" ]; then
|
|
63
|
+
echo "Version mismatch: pyproject.toml=$VERSION tag=$TAG"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
- name: Download distributions
|
|
68
|
+
uses: actions/download-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: distributions
|
|
71
|
+
path: dist
|
|
72
|
+
|
|
73
|
+
- name: Inspect distributions
|
|
74
|
+
run: find dist -maxdepth 1 -type f
|
|
75
|
+
|
|
76
|
+
- name: Publish to PyPI
|
|
77
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
78
|
+
with:
|
|
79
|
+
packages-dir: dist
|
|
80
|
+
skip-existing: true
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
.pytest_cache/
|
|
3
|
+
.ruff_cache/
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*.egg-info/
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
pytyr-mcp-output/
|
|
10
|
+
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
ENV/
|
|
14
|
+
.env
|
|
15
|
+
.env.*
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.pyre/
|
|
18
|
+
.coverage
|
|
19
|
+
coverage.xml
|
|
20
|
+
htmlcov/
|
|
21
|
+
.eggs/
|
|
22
|
+
.wheelhouse/
|
|
23
|
+
.DS_Store
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
*.swp
|
|
27
|
+
*.swo
|
pytyr_mcp-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytyr-mcp
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: MCP server exposing pytyr planning tools for planning-and-learning agents
|
|
5
|
+
Author: Dominik Drexler
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Requires-Dist: fastmcp>=2
|
|
8
|
+
Requires-Dist: pytyr<0.1,>=0.0.24
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
11
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# pytyr-mcp
|
|
15
|
+
|
|
16
|
+
MCP server exposing pytyr planning tools for planning-and-learning agents.
|
|
17
|
+
|
|
18
|
+
## Roles
|
|
19
|
+
|
|
20
|
+
Set `PYTYR_MCP_ROLE` before launching the server. The server and invoke CLI fail closed when the role is missing, so restricted agents must be launched with an explicit role:
|
|
21
|
+
|
|
22
|
+
- `planning/sample`: generator inspection and sampling tools.
|
|
23
|
+
- `planning/solvability`: solvability proof tool.
|
|
24
|
+
- `planning`: both sampling and solvability planning tools.
|
|
25
|
+
- `all`: every pytyr MCP tool; use only for trusted, unrestricted local maintenance.
|
|
26
|
+
|
|
27
|
+
Slash roles also accept dotted aliases such as `planning.solvability`. The server rejects missing or unknown roles at startup.
|
|
28
|
+
|
|
29
|
+
## Output Contract
|
|
30
|
+
|
|
31
|
+
Sampling and solvability tools write layered artifacts under the requested `output_dir`. If that directory already contains output, the tool allocates a numbered child directory such as `run-002` instead of overwriting. Results include `primary` orchestration fields, a structured `summary`, and `items` with relative paths to generated tasks, invalid configs, or proof artifacts.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# pytyr-mcp
|
|
2
|
+
|
|
3
|
+
MCP server exposing pytyr planning tools for planning-and-learning agents.
|
|
4
|
+
|
|
5
|
+
## Roles
|
|
6
|
+
|
|
7
|
+
Set `PYTYR_MCP_ROLE` before launching the server. The server and invoke CLI fail closed when the role is missing, so restricted agents must be launched with an explicit role:
|
|
8
|
+
|
|
9
|
+
- `planning/sample`: generator inspection and sampling tools.
|
|
10
|
+
- `planning/solvability`: solvability proof tool.
|
|
11
|
+
- `planning`: both sampling and solvability planning tools.
|
|
12
|
+
- `all`: every pytyr MCP tool; use only for trusted, unrestricted local maintenance.
|
|
13
|
+
|
|
14
|
+
Slash roles also accept dotted aliases such as `planning.solvability`. The server rejects missing or unknown roles at startup.
|
|
15
|
+
|
|
16
|
+
## Output Contract
|
|
17
|
+
|
|
18
|
+
Sampling and solvability tools write layered artifacts under the requested `output_dir`. If that directory already contains output, the tool allocates a numbered child directory such as `run-002` instead of overwriting. Results include `primary` orchestration fields, a structured `summary`, and `items` with relative paths to generated tasks, invalid configs, or proof artifacts.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytyr-mcp"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "MCP server exposing pytyr planning tools for planning-and-learning agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Dominik Drexler"},
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"fastmcp>=2",
|
|
16
|
+
"pytyr>=0.0.24,<0.1",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8",
|
|
22
|
+
"ruff>=0.8",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
pytyr-mcp-invoke = "pytyr_mcp.invoke:main"
|
|
27
|
+
pytyr-mcp = "pytyr_mcp.server:main"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/pytyr_mcp"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
pythonpath = ["src"]
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 100
|
|
38
|
+
target-version = "py313"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pytyr_mcp.paths import relative_to
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def write_json(path: Path, data: Any) -> None:
|
|
11
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
with path.open("x", encoding="utf-8") as fh:
|
|
13
|
+
fh.write(json.dumps(data, indent=2, sort_keys=True) + "\n")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
RESERVATION_MARKER = ".pytyr-mcp-output"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def has_existing_output(output_dir: Path) -> bool:
|
|
20
|
+
return any(
|
|
21
|
+
(output_dir / name).exists()
|
|
22
|
+
for name in (RESERVATION_MARKER, "summary.json", "summary.md", "raw", "tasks", "domain.pddl")
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _reserve_output_dir(output_dir: Path) -> bool:
|
|
27
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
if has_existing_output(output_dir):
|
|
29
|
+
return False
|
|
30
|
+
try:
|
|
31
|
+
with (output_dir / RESERVATION_MARKER).open("x", encoding="utf-8") as fh:
|
|
32
|
+
fh.write("reserved\n")
|
|
33
|
+
except FileExistsError:
|
|
34
|
+
return False
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def fresh_output_dir(output_dir: Path) -> Path:
|
|
39
|
+
if _reserve_output_dir(output_dir):
|
|
40
|
+
return output_dir
|
|
41
|
+
for index in range(2, 10000):
|
|
42
|
+
candidate = output_dir / f"run-{index:03d}"
|
|
43
|
+
if _reserve_output_dir(candidate):
|
|
44
|
+
return candidate
|
|
45
|
+
raise RuntimeError(f"could not allocate fresh Tyr MCP output directory under {output_dir}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _summary_task_path(task: dict[str, Any], output_dir: Path) -> str:
|
|
49
|
+
raw_path = task.get("path")
|
|
50
|
+
if raw_path is None:
|
|
51
|
+
return ""
|
|
52
|
+
path = Path(str(raw_path))
|
|
53
|
+
if not path.is_absolute():
|
|
54
|
+
return path.as_posix()
|
|
55
|
+
try:
|
|
56
|
+
return path.relative_to(output_dir).as_posix()
|
|
57
|
+
except ValueError:
|
|
58
|
+
return "<omitted: outside output_dir>"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def write_solvability_summary(
|
|
62
|
+
*,
|
|
63
|
+
tool: str,
|
|
64
|
+
status: str,
|
|
65
|
+
output_dir: Path,
|
|
66
|
+
metadata: dict[str, Any],
|
|
67
|
+
tasks: list[dict[str, Any]],
|
|
68
|
+
stdout: str = "",
|
|
69
|
+
stderr: str = "",
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
output_dir = fresh_output_dir(output_dir)
|
|
72
|
+
raw_dir = output_dir / "raw"
|
|
73
|
+
raw_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
with (raw_dir / "stdout.txt").open("x", encoding="utf-8") as fh:
|
|
75
|
+
fh.write(stdout)
|
|
76
|
+
with (raw_dir / "stderr.txt").open("x", encoding="utf-8") as fh:
|
|
77
|
+
fh.write(stderr)
|
|
78
|
+
|
|
79
|
+
by_status: dict[str, list[dict[str, Any]]] = {}
|
|
80
|
+
for task in tasks:
|
|
81
|
+
by_status.setdefault(str(task["status"]), []).append(task)
|
|
82
|
+
|
|
83
|
+
task_dir = output_dir / "tasks"
|
|
84
|
+
task_items = []
|
|
85
|
+
for index, task in enumerate(tasks, start=1):
|
|
86
|
+
task_id = f"task-{index:03d}"
|
|
87
|
+
task_path = task_dir / f"{task_id}.json"
|
|
88
|
+
task_data = dict(task)
|
|
89
|
+
task_data["path"] = _summary_task_path(task, output_dir)
|
|
90
|
+
task_data.setdefault("schema_version", 1)
|
|
91
|
+
task_data.setdefault("id", task_id)
|
|
92
|
+
write_json(task_path, task_data)
|
|
93
|
+
task_items.append(
|
|
94
|
+
{
|
|
95
|
+
"id": task_id,
|
|
96
|
+
"name": task["name"],
|
|
97
|
+
"status": task["status"],
|
|
98
|
+
"path": relative_to(task_path, output_dir),
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
summary = {
|
|
103
|
+
"schema_version": 1,
|
|
104
|
+
"tool": tool,
|
|
105
|
+
"status": status,
|
|
106
|
+
"metadata": metadata,
|
|
107
|
+
"counts": {
|
|
108
|
+
"tasks": len(tasks),
|
|
109
|
+
"solved": sum(1 for task in tasks if task.get("solved")),
|
|
110
|
+
"unsolved": sum(1 for task in tasks if not task.get("solved")),
|
|
111
|
+
"statuses": len(by_status),
|
|
112
|
+
},
|
|
113
|
+
"by_status": {
|
|
114
|
+
status_name: {
|
|
115
|
+
"count": len(values),
|
|
116
|
+
"tasks": [
|
|
117
|
+
{
|
|
118
|
+
"name": task["name"],
|
|
119
|
+
"path": _summary_task_path(task, output_dir),
|
|
120
|
+
"plan_length": task.get("plan_length"),
|
|
121
|
+
}
|
|
122
|
+
for task in values
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
for status_name, values in sorted(by_status.items())
|
|
126
|
+
},
|
|
127
|
+
"tasks": task_items,
|
|
128
|
+
"raw": {
|
|
129
|
+
"stdout_path": "raw/stdout.txt",
|
|
130
|
+
"stderr_path": "raw/stderr.txt",
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
write_json(output_dir / "summary.json", summary)
|
|
134
|
+
write_solvability_markdown(output_dir / "summary.md", summary)
|
|
135
|
+
primary = {
|
|
136
|
+
"successful": status == "success",
|
|
137
|
+
"all_solved": status == "success",
|
|
138
|
+
"solved": [task["name"] for task in tasks if task.get("solved")],
|
|
139
|
+
"unsolved": [task["name"] for task in tasks if not task.get("solved")],
|
|
140
|
+
"task_statuses": [(task["name"], task["status"]) for task in tasks],
|
|
141
|
+
}
|
|
142
|
+
artifacts = {
|
|
143
|
+
"summary_json": relative_to(output_dir / "summary.json", output_dir),
|
|
144
|
+
"summary_md": relative_to(output_dir / "summary.md", output_dir),
|
|
145
|
+
"raw_stdout": "raw/stdout.txt",
|
|
146
|
+
"raw_stderr": "raw/stderr.txt",
|
|
147
|
+
"output_dir": output_dir.as_posix(),
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
"schema_version": summary["schema_version"],
|
|
151
|
+
"tool": tool,
|
|
152
|
+
"status": status,
|
|
153
|
+
"primary": primary,
|
|
154
|
+
"summary": summary,
|
|
155
|
+
"artifacts": artifacts,
|
|
156
|
+
"items": task_items,
|
|
157
|
+
"tasks": summary["tasks"],
|
|
158
|
+
"summary_path": artifacts["summary_json"],
|
|
159
|
+
"summary_md_path": artifacts["summary_md"],
|
|
160
|
+
"output_dir": output_dir.as_posix(),
|
|
161
|
+
"counts": summary["counts"],
|
|
162
|
+
"by_status": summary["by_status"],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def write_solvability_markdown(path: Path, summary: dict[str, Any]) -> None:
|
|
167
|
+
lines = [
|
|
168
|
+
f"# {summary['tool']}",
|
|
169
|
+
"",
|
|
170
|
+
f"Status: `{summary['status']}`",
|
|
171
|
+
"",
|
|
172
|
+
"## Counts",
|
|
173
|
+
"",
|
|
174
|
+
f"- Tasks: {summary['counts']['tasks']}",
|
|
175
|
+
f"- Solved: {summary['counts']['solved']}",
|
|
176
|
+
f"- Unsolved: {summary['counts']['unsolved']}",
|
|
177
|
+
"",
|
|
178
|
+
"## Tasks",
|
|
179
|
+
"",
|
|
180
|
+
]
|
|
181
|
+
for item in summary["tasks"]:
|
|
182
|
+
lines.append(f"- `{item['name']}`: `{item['status']}` (`{item['path']}`)")
|
|
183
|
+
with path.open("x", encoding="utf-8") as fh:
|
|
184
|
+
fh.write("\n".join(lines).rstrip() + "\n")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _default_workspace_root() -> Path:
|
|
9
|
+
return Path(__file__).resolve().parents[3]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ServerConfig:
|
|
14
|
+
workspace_root: Path
|
|
15
|
+
output_root: Path
|
|
16
|
+
default_timeout_seconds: float = 600.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_config() -> ServerConfig:
|
|
20
|
+
workspace_root = Path(os.environ.get("PYTYR_MCP_WORKSPACE", _default_workspace_root())).resolve()
|
|
21
|
+
output_root = Path(os.environ.get("PYTYR_MCP_OUTPUT_ROOT", workspace_root / "pytyr-mcp-output")).resolve()
|
|
22
|
+
return ServerConfig(
|
|
23
|
+
workspace_root=workspace_root,
|
|
24
|
+
output_root=output_root,
|
|
25
|
+
default_timeout_seconds=float(os.environ.get("PYTYR_MCP_TIMEOUT", "600")),
|
|
26
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
from contextlib import redirect_stdout
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
from pytyr_mcp.planning.sample_generator.results import describe_generator_result, sample_generator_result
|
|
11
|
+
from pytyr_mcp.planning.sample_generator.service import (
|
|
12
|
+
SampleGeneratorOptions,
|
|
13
|
+
describe_make_problem,
|
|
14
|
+
get_generator_path,
|
|
15
|
+
sample_generator,
|
|
16
|
+
)
|
|
17
|
+
from pytyr_mcp.planning.solvability.schemas import ProveSolvabilityOptions
|
|
18
|
+
from pytyr_mcp.planning.solvability.service import prove_solvability
|
|
19
|
+
from pytyr_mcp.roles import load_role
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _sample(args: dict[str, Any]) -> dict[str, Any]:
|
|
23
|
+
result = sample_generator(
|
|
24
|
+
SampleGeneratorOptions(
|
|
25
|
+
domain_name=args["domain_name"],
|
|
26
|
+
output_dir=Path(args["output_dir"]).resolve(),
|
|
27
|
+
batch_name=args.get("batch_name", "sample"),
|
|
28
|
+
configs=args.get("configs", []),
|
|
29
|
+
allow_invalid=bool(args.get("allow_invalid", False)),
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
return sample_generator_result(result)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _describe(args: dict[str, Any]) -> dict[str, Any]:
|
|
36
|
+
domain_name = args["domain_name"]
|
|
37
|
+
return describe_generator_result(
|
|
38
|
+
domain_name=domain_name,
|
|
39
|
+
generator_path=get_generator_path(domain_name),
|
|
40
|
+
signature=describe_make_problem(domain_name),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _solvability(args: dict[str, Any]) -> dict[str, Any]:
|
|
45
|
+
return prove_solvability(
|
|
46
|
+
ProveSolvabilityOptions(
|
|
47
|
+
domain=args["domain"],
|
|
48
|
+
problem_dir=args["problem_dir"],
|
|
49
|
+
output_dir=args["output_dir"],
|
|
50
|
+
num_threads=int(args.get("num_threads", 1)),
|
|
51
|
+
max_num_states=int(args.get("max_num_states", 100_000)),
|
|
52
|
+
max_time_seconds=float(args.get("max_time_seconds", args.get("max_time", 5.0))),
|
|
53
|
+
include_plans=bool(args.get("include_plans", args.get("print_plan", False))),
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
TOOLS: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
|
|
59
|
+
"tyr.planning.describe_generator": _describe,
|
|
60
|
+
"tyr.planning.sample_generator": _sample,
|
|
61
|
+
"tyr.planning.prove_solvability": _solvability,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _write_result_json(path: Path, rendered: str) -> None:
|
|
66
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
with path.open("x", encoding="utf-8") as fh:
|
|
68
|
+
fh.write(rendered)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _ensure_tool_allowed(tool_name: str) -> None:
|
|
72
|
+
role = load_role()
|
|
73
|
+
if role.allows(tool_name):
|
|
74
|
+
return
|
|
75
|
+
allowed = ", ".join(sorted(role.allowed_tools))
|
|
76
|
+
raise PermissionError(
|
|
77
|
+
f"{tool_name} is not allowed for PYTYR_MCP_ROLE={role.name}; "
|
|
78
|
+
f"allowed tools: {allowed}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main() -> None:
|
|
83
|
+
parser = argparse.ArgumentParser(description="Invoke a pytyr-mcp tool with JSON arguments.")
|
|
84
|
+
parser.add_argument("tool", choices=sorted(TOOLS))
|
|
85
|
+
parser.add_argument("--args-json", required=True)
|
|
86
|
+
parser.add_argument("--result-json")
|
|
87
|
+
parsed = parser.parse_args()
|
|
88
|
+
try:
|
|
89
|
+
_ensure_tool_allowed(parsed.tool)
|
|
90
|
+
except (PermissionError, ValueError) as exc:
|
|
91
|
+
parser.error(str(exc))
|
|
92
|
+
args = json.loads(parsed.args_json)
|
|
93
|
+
if not isinstance(args, dict):
|
|
94
|
+
raise TypeError("--args-json must decode to an object")
|
|
95
|
+
captured_stdout = io.StringIO()
|
|
96
|
+
with redirect_stdout(captured_stdout):
|
|
97
|
+
result = TOOLS[parsed.tool](args)
|
|
98
|
+
tool_stdout = captured_stdout.getvalue()
|
|
99
|
+
if tool_stdout:
|
|
100
|
+
result = {**result, "_tool_stdout": tool_stdout}
|
|
101
|
+
rendered = json.dumps(result, indent=2, sort_keys=True) + "\n"
|
|
102
|
+
if parsed.result_json:
|
|
103
|
+
result_path = Path(parsed.result_json).resolve()
|
|
104
|
+
_write_result_json(result_path, rendered)
|
|
105
|
+
else:
|
|
106
|
+
print(rendered, end="")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def server_output_dir(output_root: Path, output_dir: str | Path) -> Path:
|
|
7
|
+
root = output_root.resolve()
|
|
8
|
+
requested = Path(output_dir)
|
|
9
|
+
resolved = requested.resolve() if requested.is_absolute() else (root / requested).resolve()
|
|
10
|
+
try:
|
|
11
|
+
resolved.relative_to(root)
|
|
12
|
+
except ValueError as exc:
|
|
13
|
+
raise ValueError(f"output_dir must resolve under configured output_root: {root}") from exc
|
|
14
|
+
return resolved
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def relative_to(path: Path, root: Path) -> str:
|
|
18
|
+
try:
|
|
19
|
+
return path.relative_to(root).as_posix()
|
|
20
|
+
except ValueError:
|
|
21
|
+
return path.as_posix()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Planning tools."""
|
|
File without changes
|