pmcontrols-mcp 0.1.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.
- pmcontrols_mcp-0.1.0/LICENSE +21 -0
- pmcontrols_mcp-0.1.0/PKG-INFO +122 -0
- pmcontrols_mcp-0.1.0/README.md +92 -0
- pmcontrols_mcp-0.1.0/pmcontrols_mcp/__init__.py +8 -0
- pmcontrols_mcp-0.1.0/pmcontrols_mcp/__main__.py +4 -0
- pmcontrols_mcp-0.1.0/pmcontrols_mcp/_tools.py +118 -0
- pmcontrols_mcp-0.1.0/pmcontrols_mcp/server.py +43 -0
- pmcontrols_mcp-0.1.0/pmcontrols_mcp.egg-info/PKG-INFO +122 -0
- pmcontrols_mcp-0.1.0/pmcontrols_mcp.egg-info/SOURCES.txt +14 -0
- pmcontrols_mcp-0.1.0/pmcontrols_mcp.egg-info/dependency_links.txt +1 -0
- pmcontrols_mcp-0.1.0/pmcontrols_mcp.egg-info/entry_points.txt +2 -0
- pmcontrols_mcp-0.1.0/pmcontrols_mcp.egg-info/requires.txt +7 -0
- pmcontrols_mcp-0.1.0/pmcontrols_mcp.egg-info/top_level.txt +1 -0
- pmcontrols_mcp-0.1.0/pyproject.toml +49 -0
- pmcontrols_mcp-0.1.0/setup.cfg +4 -0
- pmcontrols_mcp-0.1.0/tests/test_tools.py +69 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atakan Arikan
|
|
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,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pmcontrols-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for pmcontrols: validated project scheduling and earned value tools for AI agents.
|
|
5
|
+
Author: Atakan Arikan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/arikanatakan/pmcontrols-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/arikanatakan/pmcontrols-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/arikanatakan/pmcontrols-mcp/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,ai-agents,project-management,critical-path,cpm,pert,earned-value,earned-schedule,project-controls
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Office/Business :: Scheduling
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pmcontrols>=0.1.0
|
|
24
|
+
Requires-Dist: mcp>=1.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
|
+
Requires-Dist: build; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
<!-- mcp-name: io.github.arikanatakan/pmcontrols-mcp -->
|
|
32
|
+
|
|
33
|
+
# pmcontrols-mcp
|
|
34
|
+
|
|
35
|
+
[](https://github.com/arikanatakan/pmcontrols-mcp/actions/workflows/ci.yml)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
|
|
38
|
+
An MCP server that exposes [pmcontrols](https://github.com/arikanatakan/pmcontrols),
|
|
39
|
+
the validated project scheduling and earned value library for Python, as tools
|
|
40
|
+
for AI agents.
|
|
41
|
+
|
|
42
|
+
Agents asked to plan a project or report its status tend to generate the
|
|
43
|
+
arithmetic themselves: a backward pass done by eye, an earned-value index
|
|
44
|
+
inverted, an earned schedule mistaken for schedule variance. Generated project
|
|
45
|
+
metrics fail silently. The calculation belongs in a deterministic, versioned,
|
|
46
|
+
validated library that the agent calls, which leaves the agent to choose the
|
|
47
|
+
analysis and explain the result.
|
|
48
|
+
|
|
49
|
+
## Tools
|
|
50
|
+
|
|
51
|
+
| Tool | Purpose |
|
|
52
|
+
| ---- | ------- |
|
|
53
|
+
| `critical_path` | CPM forward and backward pass: ES, EF, LS, LF, slack, critical path |
|
|
54
|
+
| `schedule_risk` | PERT three-point analysis with a Monte Carlo completion distribution and criticality indices |
|
|
55
|
+
| `crash_schedule` | minimum-cost schedule compression to a deadline, solved as a linear program |
|
|
56
|
+
| `earned_value` | the full EVM indicator set with Lipke earned schedule, against a planned-value baseline |
|
|
57
|
+
| `earned_schedule` | the earned schedule for a given earned value |
|
|
58
|
+
|
|
59
|
+
Each tool returns the library's structured payload: named statistics, a tidy
|
|
60
|
+
table, structured alerts, and provenance (library version, input hash,
|
|
61
|
+
timestamp).
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
pip install pmcontrols-mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or run it without installing, with [uv](https://docs.astral.sh/uv/):
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
uvx pmcontrols-mcp
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
Add the server to your MCP client's configuration:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"pmcontrols": {
|
|
83
|
+
"command": "pmcontrols-mcp"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The server communicates over stdio and works with any MCP-compatible client.
|
|
90
|
+
|
|
91
|
+
## Example
|
|
92
|
+
|
|
93
|
+
Calling `critical_path` with a list of activities returns a structured
|
|
94
|
+
result the agent reads directly, instead of computing the schedule itself:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"method": "cpm",
|
|
99
|
+
"stats": {"project_duration": 15.0, "n_activities": 8.0, "n_critical": 5.0},
|
|
100
|
+
"meta": {
|
|
101
|
+
"critical_activities": ["A", "C", "E", "G", "H"],
|
|
102
|
+
"version": "0.1.0",
|
|
103
|
+
"input_hash": "sha256:...",
|
|
104
|
+
"computed_at": "2026-06-15T09:14:02+00:00"
|
|
105
|
+
},
|
|
106
|
+
"table": {"activity": ["A", "B", "..."], "slack": [0.0, 1.0, "..."]}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Every result carries provenance (library version, input hash, timestamp), so
|
|
111
|
+
a figure an agent reports can be recomputed and audited later.
|
|
112
|
+
|
|
113
|
+
## Design
|
|
114
|
+
|
|
115
|
+
The reasoning behind routing project-control arithmetic through a validated
|
|
116
|
+
tool, rather than letting a model generate it, is set out in
|
|
117
|
+
[Project control is not a language task](https://arikanatakan.github.io/pmcontrols/agents/).
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT. Written and maintained by [Atakan Arikan](https://github.com/arikanatakan),
|
|
122
|
+
MSc Student at Tsinghua University and Politecnico di Milano.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.arikanatakan/pmcontrols-mcp -->
|
|
2
|
+
|
|
3
|
+
# pmcontrols-mcp
|
|
4
|
+
|
|
5
|
+
[](https://github.com/arikanatakan/pmcontrols-mcp/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
An MCP server that exposes [pmcontrols](https://github.com/arikanatakan/pmcontrols),
|
|
9
|
+
the validated project scheduling and earned value library for Python, as tools
|
|
10
|
+
for AI agents.
|
|
11
|
+
|
|
12
|
+
Agents asked to plan a project or report its status tend to generate the
|
|
13
|
+
arithmetic themselves: a backward pass done by eye, an earned-value index
|
|
14
|
+
inverted, an earned schedule mistaken for schedule variance. Generated project
|
|
15
|
+
metrics fail silently. The calculation belongs in a deterministic, versioned,
|
|
16
|
+
validated library that the agent calls, which leaves the agent to choose the
|
|
17
|
+
analysis and explain the result.
|
|
18
|
+
|
|
19
|
+
## Tools
|
|
20
|
+
|
|
21
|
+
| Tool | Purpose |
|
|
22
|
+
| ---- | ------- |
|
|
23
|
+
| `critical_path` | CPM forward and backward pass: ES, EF, LS, LF, slack, critical path |
|
|
24
|
+
| `schedule_risk` | PERT three-point analysis with a Monte Carlo completion distribution and criticality indices |
|
|
25
|
+
| `crash_schedule` | minimum-cost schedule compression to a deadline, solved as a linear program |
|
|
26
|
+
| `earned_value` | the full EVM indicator set with Lipke earned schedule, against a planned-value baseline |
|
|
27
|
+
| `earned_schedule` | the earned schedule for a given earned value |
|
|
28
|
+
|
|
29
|
+
Each tool returns the library's structured payload: named statistics, a tidy
|
|
30
|
+
table, structured alerts, and provenance (library version, input hash,
|
|
31
|
+
timestamp).
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
pip install pmcontrols-mcp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or run it without installing, with [uv](https://docs.astral.sh/uv/):
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
uvx pmcontrols-mcp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
Add the server to your MCP client's configuration:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"pmcontrols": {
|
|
53
|
+
"command": "pmcontrols-mcp"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The server communicates over stdio and works with any MCP-compatible client.
|
|
60
|
+
|
|
61
|
+
## Example
|
|
62
|
+
|
|
63
|
+
Calling `critical_path` with a list of activities returns a structured
|
|
64
|
+
result the agent reads directly, instead of computing the schedule itself:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"method": "cpm",
|
|
69
|
+
"stats": {"project_duration": 15.0, "n_activities": 8.0, "n_critical": 5.0},
|
|
70
|
+
"meta": {
|
|
71
|
+
"critical_activities": ["A", "C", "E", "G", "H"],
|
|
72
|
+
"version": "0.1.0",
|
|
73
|
+
"input_hash": "sha256:...",
|
|
74
|
+
"computed_at": "2026-06-15T09:14:02+00:00"
|
|
75
|
+
},
|
|
76
|
+
"table": {"activity": ["A", "B", "..."], "slack": [0.0, 1.0, "..."]}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Every result carries provenance (library version, input hash, timestamp), so
|
|
81
|
+
a figure an agent reports can be recomputed and audited later.
|
|
82
|
+
|
|
83
|
+
## Design
|
|
84
|
+
|
|
85
|
+
The reasoning behind routing project-control arithmetic through a validated
|
|
86
|
+
tool, rather than letting a model generate it, is set out in
|
|
87
|
+
[Project control is not a language task](https://arikanatakan.github.io/pmcontrols/agents/).
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT. Written and maintained by [Atakan Arikan](https://github.com/arikanatakan),
|
|
92
|
+
MSc Student at Tsinghua University and Politecnico di Milano.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""pmcontrols-mcp: project scheduling and earned value tools for AI agents.
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol server that exposes pmcontrols (critical path,
|
|
4
|
+
PERT, schedule crashing, earned value, earned schedule) as validated tools,
|
|
5
|
+
so an agent calls a checked function instead of doing the math in tokens.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Tool functions and their input schemas, kept free of the MCP SDK so they
|
|
2
|
+
can be tested directly.
|
|
3
|
+
|
|
4
|
+
Each activity type is a typed model, so the server advertises exactly which
|
|
5
|
+
fields an agent must supply. Each tool calls pmcontrols and returns the
|
|
6
|
+
analysis as a JSON-safe dict (``Result.to_dict()``) with stats, a tidy
|
|
7
|
+
table, structured alerts, and provenance.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import pmcontrols as pm
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CpmActivity(BaseModel):
|
|
17
|
+
"""One activity in a deterministic schedule network."""
|
|
18
|
+
|
|
19
|
+
id: str = Field(description="Unique activity identifier.")
|
|
20
|
+
predecessors: list[str] = Field(
|
|
21
|
+
default_factory=list,
|
|
22
|
+
description="Ids of the immediate predecessor activities; empty to start.",
|
|
23
|
+
)
|
|
24
|
+
duration: float = Field(description="Activity duration in consistent time units.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PertActivity(BaseModel):
|
|
28
|
+
"""One activity with a three-point (optimistic/likely/pessimistic) estimate."""
|
|
29
|
+
|
|
30
|
+
id: str = Field(description="Unique activity identifier.")
|
|
31
|
+
predecessors: list[str] = Field(
|
|
32
|
+
default_factory=list,
|
|
33
|
+
description="Ids of the immediate predecessor activities; empty to start.",
|
|
34
|
+
)
|
|
35
|
+
a: float = Field(description="Optimistic duration.")
|
|
36
|
+
m: float = Field(description="Most likely duration.")
|
|
37
|
+
b: float = Field(description="Pessimistic duration.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CrashActivity(BaseModel):
|
|
41
|
+
"""One activity with normal and fully-crashed durations and a crash cost."""
|
|
42
|
+
|
|
43
|
+
id: str = Field(description="Unique activity identifier.")
|
|
44
|
+
predecessors: list[str] = Field(
|
|
45
|
+
default_factory=list,
|
|
46
|
+
description="Ids of the immediate predecessor activities; empty to start.",
|
|
47
|
+
)
|
|
48
|
+
duration: float = Field(description="Normal duration.")
|
|
49
|
+
crash_duration: float = Field(description="Fully-crashed (minimum) duration.")
|
|
50
|
+
crash_cost: float = Field(description="Crash cost per period of compression.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _as_dicts(items: list) -> list[dict]:
|
|
54
|
+
"""Accept either pydantic models (from the MCP layer) or plain dicts."""
|
|
55
|
+
return [i if isinstance(i, dict) else i.model_dump() for i in items]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def critical_path(activities: list[CpmActivity]) -> dict:
|
|
59
|
+
"""Critical path analysis (CPM) of an activity-on-node network.
|
|
60
|
+
|
|
61
|
+
Runs the forward and backward pass and returns earliest/latest start and
|
|
62
|
+
finish, total slack, and the zero-float critical path.
|
|
63
|
+
"""
|
|
64
|
+
return pm.cpm(_as_dicts(activities)).to_dict()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def schedule_risk(
|
|
68
|
+
activities: list[PertActivity],
|
|
69
|
+
n_sim: int = 20000,
|
|
70
|
+
seed: int | None = 0,
|
|
71
|
+
) -> dict:
|
|
72
|
+
"""PERT three-point analysis with a Monte Carlo schedule-risk simulation.
|
|
73
|
+
|
|
74
|
+
Returns the analytic expected duration and standard deviation, the
|
|
75
|
+
simulated completion distribution (p50/p80/p95), and a per-activity
|
|
76
|
+
criticality index. Pass ``seed=null`` for a fresh random draw.
|
|
77
|
+
"""
|
|
78
|
+
return pm.pert(_as_dicts(activities), n_sim=n_sim, seed=seed).to_dict()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def crash_schedule(activities: list[CrashActivity], target: float) -> dict:
|
|
82
|
+
"""Minimum-cost schedule compression to a target completion time.
|
|
83
|
+
|
|
84
|
+
Returns the globally optimal crash amount per activity, the resulting
|
|
85
|
+
schedule, and the total crash cost, solved as a linear program rather
|
|
86
|
+
than by greedy marginal-cost crashing.
|
|
87
|
+
"""
|
|
88
|
+
return pm.crash(_as_dicts(activities), target=target).to_dict()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def earned_value(
|
|
92
|
+
periods: list[float],
|
|
93
|
+
pv: list[float],
|
|
94
|
+
ev: float,
|
|
95
|
+
ac: float,
|
|
96
|
+
at: float,
|
|
97
|
+
thresholds: dict[str, float] | None = None,
|
|
98
|
+
) -> dict:
|
|
99
|
+
"""Earned value status against a time-phased planned-value baseline.
|
|
100
|
+
|
|
101
|
+
``periods`` and ``pv`` define the cumulative planned-value curve. Given
|
|
102
|
+
earned value ``ev``, actual cost ``ac`` and actual time ``at``, returns
|
|
103
|
+
the full indicator set (CV, SV, CPI, SPI, the EAC family, TCPI, VAC) plus
|
|
104
|
+
Lipke's earned schedule (ES, SPI(t), IEAC(t)) and any threshold alerts.
|
|
105
|
+
"""
|
|
106
|
+
pmb = pm.plan(periods, pv)
|
|
107
|
+
return pm.evm(pmb, ev=ev, ac=ac, at=at, thresholds=thresholds).to_dict()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def earned_schedule(periods: list[float], pv: list[float], ev: float) -> dict:
|
|
111
|
+
"""Lipke earned schedule (ES) for a given earned value.
|
|
112
|
+
|
|
113
|
+
``periods`` and ``pv`` define the cumulative planned-value curve. Returns
|
|
114
|
+
the time at which the plan had earned what has now been earned, by linear
|
|
115
|
+
interpolation on the curve.
|
|
116
|
+
"""
|
|
117
|
+
pmb = pm.plan(periods, pv)
|
|
118
|
+
return {"es": pm.earned_schedule(pmb, ev)}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""The MCP server: registers the pmcontrols tools and runs over stdio.
|
|
2
|
+
|
|
3
|
+
All tools are pure, read-only computations, marked with annotations so a
|
|
4
|
+
client can present and auto-run them safely.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from mcp.server.fastmcp import FastMCP
|
|
10
|
+
from mcp.types import ToolAnnotations
|
|
11
|
+
|
|
12
|
+
from . import _tools
|
|
13
|
+
|
|
14
|
+
mcp = FastMCP("pmcontrols")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _annotations(title: str, idempotent: bool = True) -> ToolAnnotations:
|
|
18
|
+
return ToolAnnotations(
|
|
19
|
+
title=title,
|
|
20
|
+
readOnlyHint=True,
|
|
21
|
+
idempotentHint=idempotent,
|
|
22
|
+
openWorldHint=False,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
mcp.tool(annotations=_annotations("Critical path (CPM)"))(_tools.critical_path)
|
|
27
|
+
mcp.tool(
|
|
28
|
+
annotations=_annotations("Schedule risk (PERT + Monte Carlo)", idempotent=False)
|
|
29
|
+
)(_tools.schedule_risk)
|
|
30
|
+
mcp.tool(annotations=_annotations("Schedule crashing (minimum-cost LP)"))(
|
|
31
|
+
_tools.crash_schedule
|
|
32
|
+
)
|
|
33
|
+
mcp.tool(annotations=_annotations("Earned value status"))(_tools.earned_value)
|
|
34
|
+
mcp.tool(annotations=_annotations("Earned schedule"))(_tools.earned_schedule)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> None:
|
|
38
|
+
"""Console-script entry point: run the server on stdio."""
|
|
39
|
+
mcp.run()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pmcontrols-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for pmcontrols: validated project scheduling and earned value tools for AI agents.
|
|
5
|
+
Author: Atakan Arikan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/arikanatakan/pmcontrols-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/arikanatakan/pmcontrols-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/arikanatakan/pmcontrols-mcp/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,ai-agents,project-management,critical-path,cpm,pert,earned-value,earned-schedule,project-controls
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Office/Business :: Scheduling
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pmcontrols>=0.1.0
|
|
24
|
+
Requires-Dist: mcp>=1.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
|
+
Requires-Dist: build; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
<!-- mcp-name: io.github.arikanatakan/pmcontrols-mcp -->
|
|
32
|
+
|
|
33
|
+
# pmcontrols-mcp
|
|
34
|
+
|
|
35
|
+
[](https://github.com/arikanatakan/pmcontrols-mcp/actions/workflows/ci.yml)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
|
|
38
|
+
An MCP server that exposes [pmcontrols](https://github.com/arikanatakan/pmcontrols),
|
|
39
|
+
the validated project scheduling and earned value library for Python, as tools
|
|
40
|
+
for AI agents.
|
|
41
|
+
|
|
42
|
+
Agents asked to plan a project or report its status tend to generate the
|
|
43
|
+
arithmetic themselves: a backward pass done by eye, an earned-value index
|
|
44
|
+
inverted, an earned schedule mistaken for schedule variance. Generated project
|
|
45
|
+
metrics fail silently. The calculation belongs in a deterministic, versioned,
|
|
46
|
+
validated library that the agent calls, which leaves the agent to choose the
|
|
47
|
+
analysis and explain the result.
|
|
48
|
+
|
|
49
|
+
## Tools
|
|
50
|
+
|
|
51
|
+
| Tool | Purpose |
|
|
52
|
+
| ---- | ------- |
|
|
53
|
+
| `critical_path` | CPM forward and backward pass: ES, EF, LS, LF, slack, critical path |
|
|
54
|
+
| `schedule_risk` | PERT three-point analysis with a Monte Carlo completion distribution and criticality indices |
|
|
55
|
+
| `crash_schedule` | minimum-cost schedule compression to a deadline, solved as a linear program |
|
|
56
|
+
| `earned_value` | the full EVM indicator set with Lipke earned schedule, against a planned-value baseline |
|
|
57
|
+
| `earned_schedule` | the earned schedule for a given earned value |
|
|
58
|
+
|
|
59
|
+
Each tool returns the library's structured payload: named statistics, a tidy
|
|
60
|
+
table, structured alerts, and provenance (library version, input hash,
|
|
61
|
+
timestamp).
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
pip install pmcontrols-mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or run it without installing, with [uv](https://docs.astral.sh/uv/):
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
uvx pmcontrols-mcp
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
Add the server to your MCP client's configuration:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"pmcontrols": {
|
|
83
|
+
"command": "pmcontrols-mcp"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The server communicates over stdio and works with any MCP-compatible client.
|
|
90
|
+
|
|
91
|
+
## Example
|
|
92
|
+
|
|
93
|
+
Calling `critical_path` with a list of activities returns a structured
|
|
94
|
+
result the agent reads directly, instead of computing the schedule itself:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"method": "cpm",
|
|
99
|
+
"stats": {"project_duration": 15.0, "n_activities": 8.0, "n_critical": 5.0},
|
|
100
|
+
"meta": {
|
|
101
|
+
"critical_activities": ["A", "C", "E", "G", "H"],
|
|
102
|
+
"version": "0.1.0",
|
|
103
|
+
"input_hash": "sha256:...",
|
|
104
|
+
"computed_at": "2026-06-15T09:14:02+00:00"
|
|
105
|
+
},
|
|
106
|
+
"table": {"activity": ["A", "B", "..."], "slack": [0.0, 1.0, "..."]}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Every result carries provenance (library version, input hash, timestamp), so
|
|
111
|
+
a figure an agent reports can be recomputed and audited later.
|
|
112
|
+
|
|
113
|
+
## Design
|
|
114
|
+
|
|
115
|
+
The reasoning behind routing project-control arithmetic through a validated
|
|
116
|
+
tool, rather than letting a model generate it, is set out in
|
|
117
|
+
[Project control is not a language task](https://arikanatakan.github.io/pmcontrols/agents/).
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT. Written and maintained by [Atakan Arikan](https://github.com/arikanatakan),
|
|
122
|
+
MSc Student at Tsinghua University and Politecnico di Milano.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pmcontrols_mcp/__init__.py
|
|
5
|
+
pmcontrols_mcp/__main__.py
|
|
6
|
+
pmcontrols_mcp/_tools.py
|
|
7
|
+
pmcontrols_mcp/server.py
|
|
8
|
+
pmcontrols_mcp.egg-info/PKG-INFO
|
|
9
|
+
pmcontrols_mcp.egg-info/SOURCES.txt
|
|
10
|
+
pmcontrols_mcp.egg-info/dependency_links.txt
|
|
11
|
+
pmcontrols_mcp.egg-info/entry_points.txt
|
|
12
|
+
pmcontrols_mcp.egg-info/requires.txt
|
|
13
|
+
pmcontrols_mcp.egg-info/top_level.txt
|
|
14
|
+
tests/test_tools.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pmcontrols_mcp
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pmcontrols-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for pmcontrols: validated project scheduling and earned value tools for AI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Atakan Arikan" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = ["pmcontrols>=0.1.0", "mcp>=1.0"]
|
|
14
|
+
keywords = [
|
|
15
|
+
"mcp", "model-context-protocol", "ai-agents", "project-management",
|
|
16
|
+
"critical-path", "cpm", "pert", "earned-value", "earned-schedule",
|
|
17
|
+
"project-controls",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Scientific/Engineering",
|
|
28
|
+
"Topic :: Office/Business :: Scheduling",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
pmcontrols-mcp = "pmcontrols_mcp.server:main"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = ["pytest>=7", "ruff", "build"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/arikanatakan/pmcontrols-mcp"
|
|
39
|
+
Repository = "https://github.com/arikanatakan/pmcontrols-mcp"
|
|
40
|
+
Issues = "https://github.com/arikanatakan/pmcontrols-mcp/issues"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
include = ["pmcontrols_mcp*"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 88
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Tests for the tool functions (no MCP client needed).
|
|
2
|
+
|
|
3
|
+
The numbers are the General Foundry network and a linear PMB, the same
|
|
4
|
+
reference cases pmcontrols itself validates against.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pmcontrols_mcp._tools as t
|
|
8
|
+
|
|
9
|
+
FOUNDRY = [
|
|
10
|
+
{"id": "A", "predecessors": [], "duration": 2},
|
|
11
|
+
{"id": "B", "predecessors": [], "duration": 3},
|
|
12
|
+
{"id": "C", "predecessors": ["A"], "duration": 2},
|
|
13
|
+
{"id": "D", "predecessors": ["B"], "duration": 4},
|
|
14
|
+
{"id": "E", "predecessors": ["C"], "duration": 4},
|
|
15
|
+
{"id": "F", "predecessors": ["C"], "duration": 3},
|
|
16
|
+
{"id": "G", "predecessors": ["D", "E"], "duration": 5},
|
|
17
|
+
{"id": "H", "predecessors": ["F", "G"], "duration": 2},
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
CRASH = [
|
|
21
|
+
{"id": "A", "predecessors": [], "duration": 2, "crash_duration": 1, "crash_cost": 1000},
|
|
22
|
+
{"id": "B", "predecessors": [], "duration": 3, "crash_duration": 1, "crash_cost": 2000},
|
|
23
|
+
{"id": "C", "predecessors": ["A"], "duration": 2, "crash_duration": 1, "crash_cost": 1000},
|
|
24
|
+
{"id": "D", "predecessors": ["B"], "duration": 4, "crash_duration": 3, "crash_cost": 1000},
|
|
25
|
+
{"id": "E", "predecessors": ["C"], "duration": 4, "crash_duration": 2, "crash_cost": 1000},
|
|
26
|
+
{"id": "F", "predecessors": ["C"], "duration": 3, "crash_duration": 2, "crash_cost": 500},
|
|
27
|
+
{"id": "G", "predecessors": ["D", "E"], "duration": 5, "crash_duration": 2, "crash_cost": 2000},
|
|
28
|
+
{"id": "H", "predecessors": ["F", "G"], "duration": 2, "crash_duration": 1, "crash_cost": 3000},
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
PERIODS = list(range(11))
|
|
32
|
+
PV = [i * 10000 for i in PERIODS]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_critical_path():
|
|
36
|
+
d = t.critical_path(FOUNDRY)
|
|
37
|
+
assert d["method"] == "cpm"
|
|
38
|
+
assert d["stats"]["project_duration"] == 15.0
|
|
39
|
+
assert d["meta"]["critical_activities"] == ["A", "C", "E", "G", "H"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_schedule_risk():
|
|
43
|
+
tp = [
|
|
44
|
+
{"id": a["id"], "predecessors": a["predecessors"],
|
|
45
|
+
"a": a["duration"] * 0.8, "m": a["duration"], "b": a["duration"] * 1.3}
|
|
46
|
+
for a in FOUNDRY
|
|
47
|
+
]
|
|
48
|
+
d = t.schedule_risk(tp, n_sim=2000, seed=1)
|
|
49
|
+
assert d["method"] == "pert"
|
|
50
|
+
assert "mc_p80" in d["stats"]
|
|
51
|
+
assert "criticality_index" in d["table"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_crash_schedule():
|
|
55
|
+
d = t.crash_schedule(CRASH, target=13)
|
|
56
|
+
assert d["method"] == "crash"
|
|
57
|
+
assert d["stats"]["total_crash_cost"] == 3000.0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_earned_value():
|
|
61
|
+
d = t.earned_value(PERIODS, PV, ev=30000, ac=35000, at=4)
|
|
62
|
+
assert d["method"] == "evm"
|
|
63
|
+
assert round(d["stats"]["cpi"], 4) == 0.8571
|
|
64
|
+
assert {a["indicator"] for a in d["alerts"]} == {"cpi", "spi_t"}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_earned_schedule():
|
|
68
|
+
d = t.earned_schedule(PERIODS, PV, ev=34000)
|
|
69
|
+
assert d["es"] == 3.4
|