pcp-mcp 0.1.0__tar.gz → 1.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.
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/PKG-INFO +22 -10
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/README.md +19 -8
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/pyproject.toml +14 -2
- pcp_mcp-1.0.1/src/pcp_mcp/AGENTS.md +70 -0
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/__init__.py +4 -0
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/client.py +28 -0
- pcp_mcp-1.0.1/src/pcp_mcp/config.py +107 -0
- pcp_mcp-1.0.1/src/pcp_mcp/context.py +104 -0
- pcp_mcp-1.0.1/src/pcp_mcp/icons.py +31 -0
- pcp_mcp-1.0.1/src/pcp_mcp/middleware.py +75 -0
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/models.py +10 -0
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/prompts/__init__.py +18 -5
- pcp_mcp-1.0.1/src/pcp_mcp/py.typed +0 -0
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/resources/catalog.py +76 -2
- pcp_mcp-1.0.1/src/pcp_mcp/resources/health.py +117 -0
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/server.py +20 -0
- pcp_mcp-1.0.1/src/pcp_mcp/tools/AGENTS.md +61 -0
- pcp_mcp-1.0.1/src/pcp_mcp/tools/metrics.py +186 -0
- pcp_mcp-1.0.1/src/pcp_mcp/tools/system.py +481 -0
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/utils/__init__.py +0 -4
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/utils/extractors.py +18 -0
- pcp_mcp-0.1.0/src/pcp_mcp/config.py +0 -50
- pcp_mcp-0.1.0/src/pcp_mcp/context.py +0 -57
- pcp_mcp-0.1.0/src/pcp_mcp/resources/health.py +0 -74
- pcp_mcp-0.1.0/src/pcp_mcp/tools/metrics.py +0 -148
- pcp_mcp-0.1.0/src/pcp_mcp/tools/system.py +0 -229
- pcp_mcp-0.1.0/src/pcp_mcp/utils/decorators.py +0 -38
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/errors.py +0 -0
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/resources/__init__.py +0 -0
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/tools/__init__.py +0 -0
- {pcp_mcp-0.1.0 → pcp_mcp-1.0.1}/src/pcp_mcp/utils/builders.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pcp-mcp
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: MCP server for Performance Co-Pilot
|
|
5
5
|
Keywords: mcp,pcp,performance-co-pilot,monitoring,model-context-protocol
|
|
6
6
|
Author: Major Hayden
|
|
7
7
|
Author-email: Major Hayden <major@mhtx.net>
|
|
8
8
|
License-Expression: MIT
|
|
9
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Intended Audience :: System Administrators
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
18
|
Classifier: Topic :: System :: Monitoring
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: cachetools>=5.0
|
|
20
21
|
Requires-Dist: fastmcp>=2.0.0
|
|
21
22
|
Requires-Dist: httpx>=0.27
|
|
22
23
|
Requires-Dist: pydantic-settings>=2.0.0
|
|
@@ -32,6 +33,14 @@ MCP server for [Performance Co-Pilot (PCP)](https://pcp.io/) metrics.
|
|
|
32
33
|
|
|
33
34
|
Query system performance metrics via the Model Context Protocol - CPU, memory, disk I/O, network, processes, and more.
|
|
34
35
|
|
|
36
|
+
📖 **[Full Documentation](https://major.github.io/pcp-mcp)** | 🚀 **[Getting Started](https://major.github.io/pcp-mcp/getting-started/)**
|
|
37
|
+
|
|
38
|
+
[](https://github.com/major/pcp-mcp/actions/workflows/ci.yml)
|
|
39
|
+
[](https://codecov.io/gh/major/pcp-mcp)
|
|
40
|
+
[](https://pypi.org/project/pcp-mcp/)
|
|
41
|
+
[](https://www.python.org/downloads/)
|
|
42
|
+
[](https://opensource.org/licenses/MIT)
|
|
43
|
+
|
|
35
44
|
## 🚀 Installation
|
|
36
45
|
|
|
37
46
|
```bash
|
|
@@ -47,15 +56,15 @@ uv add pcp-mcp
|
|
|
47
56
|
## 📋 Requirements
|
|
48
57
|
|
|
49
58
|
- **Python**: 3.10+
|
|
50
|
-
- **PCP**: Performance Co-Pilot with `pmproxy` running
|
|
59
|
+
- **PCP**: Performance Co-Pilot with `pmcd` and `pmproxy` running
|
|
51
60
|
```bash
|
|
52
61
|
# Fedora/RHEL/CentOS
|
|
53
62
|
sudo dnf install pcp
|
|
54
|
-
sudo systemctl enable --now pmproxy
|
|
63
|
+
sudo systemctl enable --now pmcd pmproxy
|
|
55
64
|
|
|
56
65
|
# Ubuntu/Debian
|
|
57
66
|
sudo apt install pcp
|
|
58
|
-
sudo systemctl enable --now pmproxy
|
|
67
|
+
sudo systemctl enable --now pmcd pmproxy
|
|
59
68
|
```
|
|
60
69
|
|
|
61
70
|
## ⚙️ Configuration
|
|
@@ -68,9 +77,12 @@ Configure via environment variables:
|
|
|
68
77
|
| `PCP_PORT` | pmproxy port | `44322` |
|
|
69
78
|
| `PCP_TARGET_HOST` | Target pmcd host to monitor | `localhost` |
|
|
70
79
|
| `PCP_USE_TLS` | Use HTTPS for pmproxy | `false` |
|
|
80
|
+
| `PCP_TLS_VERIFY` | Verify TLS certificates | `true` |
|
|
81
|
+
| `PCP_TLS_CA_BUNDLE` | Path to custom CA bundle | (optional) |
|
|
71
82
|
| `PCP_TIMEOUT` | Request timeout (seconds) | `30` |
|
|
72
83
|
| `PCP_USERNAME` | HTTP basic auth user | (optional) |
|
|
73
84
|
| `PCP_PASSWORD` | HTTP basic auth password | (optional) |
|
|
85
|
+
| `PCP_ALLOWED_HOSTS` | Hostspecs allowed via host param | (optional) |
|
|
74
86
|
|
|
75
87
|
## 🎯 Usage
|
|
76
88
|
|
|
@@ -163,9 +175,9 @@ For remote monitoring:
|
|
|
163
175
|
|
|
164
176
|
Browse metrics via MCP resources:
|
|
165
177
|
|
|
166
|
-
- `pcp://
|
|
167
|
-
- `pcp://
|
|
168
|
-
- `pcp://
|
|
178
|
+
- `pcp://health` - Quick system health summary
|
|
179
|
+
- `pcp://metrics/common` - Catalog of commonly used metrics
|
|
180
|
+
- `pcp://namespaces` - Live-discovered metric namespaces
|
|
169
181
|
|
|
170
182
|
## 💡 Use Cases
|
|
171
183
|
|
|
@@ -191,9 +203,9 @@ Ask Claude to:
|
|
|
191
203
|
## 🏗️ Architecture
|
|
192
204
|
|
|
193
205
|
```
|
|
194
|
-
┌─────────┐ ┌─────────┐
|
|
206
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
195
207
|
│ LLM │ ◄─MCP─► │ pcp-mcp │ ◄─HTTP─► │ pmproxy │ ◄─────► │ pmcd │
|
|
196
|
-
└─────────┘ └─────────┘
|
|
208
|
+
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
|
197
209
|
(REST API) (metrics)
|
|
198
210
|
```
|
|
199
211
|
|
|
@@ -4,6 +4,14 @@ MCP server for [Performance Co-Pilot (PCP)](https://pcp.io/) metrics.
|
|
|
4
4
|
|
|
5
5
|
Query system performance metrics via the Model Context Protocol - CPU, memory, disk I/O, network, processes, and more.
|
|
6
6
|
|
|
7
|
+
📖 **[Full Documentation](https://major.github.io/pcp-mcp)** | 🚀 **[Getting Started](https://major.github.io/pcp-mcp/getting-started/)**
|
|
8
|
+
|
|
9
|
+
[](https://github.com/major/pcp-mcp/actions/workflows/ci.yml)
|
|
10
|
+
[](https://codecov.io/gh/major/pcp-mcp)
|
|
11
|
+
[](https://pypi.org/project/pcp-mcp/)
|
|
12
|
+
[](https://www.python.org/downloads/)
|
|
13
|
+
[](https://opensource.org/licenses/MIT)
|
|
14
|
+
|
|
7
15
|
## 🚀 Installation
|
|
8
16
|
|
|
9
17
|
```bash
|
|
@@ -19,15 +27,15 @@ uv add pcp-mcp
|
|
|
19
27
|
## 📋 Requirements
|
|
20
28
|
|
|
21
29
|
- **Python**: 3.10+
|
|
22
|
-
- **PCP**: Performance Co-Pilot with `pmproxy` running
|
|
30
|
+
- **PCP**: Performance Co-Pilot with `pmcd` and `pmproxy` running
|
|
23
31
|
```bash
|
|
24
32
|
# Fedora/RHEL/CentOS
|
|
25
33
|
sudo dnf install pcp
|
|
26
|
-
sudo systemctl enable --now pmproxy
|
|
34
|
+
sudo systemctl enable --now pmcd pmproxy
|
|
27
35
|
|
|
28
36
|
# Ubuntu/Debian
|
|
29
37
|
sudo apt install pcp
|
|
30
|
-
sudo systemctl enable --now pmproxy
|
|
38
|
+
sudo systemctl enable --now pmcd pmproxy
|
|
31
39
|
```
|
|
32
40
|
|
|
33
41
|
## ⚙️ Configuration
|
|
@@ -40,9 +48,12 @@ Configure via environment variables:
|
|
|
40
48
|
| `PCP_PORT` | pmproxy port | `44322` |
|
|
41
49
|
| `PCP_TARGET_HOST` | Target pmcd host to monitor | `localhost` |
|
|
42
50
|
| `PCP_USE_TLS` | Use HTTPS for pmproxy | `false` |
|
|
51
|
+
| `PCP_TLS_VERIFY` | Verify TLS certificates | `true` |
|
|
52
|
+
| `PCP_TLS_CA_BUNDLE` | Path to custom CA bundle | (optional) |
|
|
43
53
|
| `PCP_TIMEOUT` | Request timeout (seconds) | `30` |
|
|
44
54
|
| `PCP_USERNAME` | HTTP basic auth user | (optional) |
|
|
45
55
|
| `PCP_PASSWORD` | HTTP basic auth password | (optional) |
|
|
56
|
+
| `PCP_ALLOWED_HOSTS` | Hostspecs allowed via host param | (optional) |
|
|
46
57
|
|
|
47
58
|
## 🎯 Usage
|
|
48
59
|
|
|
@@ -135,9 +146,9 @@ For remote monitoring:
|
|
|
135
146
|
|
|
136
147
|
Browse metrics via MCP resources:
|
|
137
148
|
|
|
138
|
-
- `pcp://
|
|
139
|
-
- `pcp://
|
|
140
|
-
- `pcp://
|
|
149
|
+
- `pcp://health` - Quick system health summary
|
|
150
|
+
- `pcp://metrics/common` - Catalog of commonly used metrics
|
|
151
|
+
- `pcp://namespaces` - Live-discovered metric namespaces
|
|
141
152
|
|
|
142
153
|
## 💡 Use Cases
|
|
143
154
|
|
|
@@ -163,9 +174,9 @@ Ask Claude to:
|
|
|
163
174
|
## 🏗️ Architecture
|
|
164
175
|
|
|
165
176
|
```
|
|
166
|
-
┌─────────┐ ┌─────────┐
|
|
177
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
167
178
|
│ LLM │ ◄─MCP─► │ pcp-mcp │ ◄─HTTP─► │ pmproxy │ ◄─────► │ pmcd │
|
|
168
|
-
└─────────┘ └─────────┘
|
|
179
|
+
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
|
169
180
|
(REST API) (metrics)
|
|
170
181
|
```
|
|
171
182
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pcp-mcp"
|
|
3
|
-
version = "0.1
|
|
3
|
+
version = "1.0.1"
|
|
4
4
|
description = "MCP server for Performance Co-Pilot"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -8,7 +8,7 @@ authors = [{ name = "Major Hayden", email = "major@mhtx.net" }]
|
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
keywords = ["mcp", "pcp", "performance-co-pilot", "monitoring", "model-context-protocol"]
|
|
10
10
|
classifiers = [
|
|
11
|
-
"Development Status ::
|
|
11
|
+
"Development Status :: 5 - Production/Stable",
|
|
12
12
|
"Intended Audience :: Developers",
|
|
13
13
|
"Intended Audience :: System Administrators",
|
|
14
14
|
"License :: OSI Approved :: MIT License",
|
|
@@ -21,6 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Typing :: Typed",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
|
+
"cachetools>=5.0",
|
|
24
25
|
"fastmcp>=2.0.0",
|
|
25
26
|
"httpx>=0.27",
|
|
26
27
|
"pydantic-settings>=2.0.0",
|
|
@@ -50,6 +51,7 @@ dev = [
|
|
|
50
51
|
"pytest-randomly>=4.0.1",
|
|
51
52
|
"mkdocs-material>=9.5",
|
|
52
53
|
"mkdocstrings[python]>=0.27",
|
|
54
|
+
"commitizen>=4.0",
|
|
53
55
|
]
|
|
54
56
|
|
|
55
57
|
[tool.uv.build-backend]
|
|
@@ -66,6 +68,7 @@ ignore = [
|
|
|
66
68
|
"D104", # Missing docstring in public package
|
|
67
69
|
"D105", # Missing docstring in magic method
|
|
68
70
|
"D107", # Missing docstring in __init__
|
|
71
|
+
"UP045", # Optional[X] vs X | None - Pydantic needs Optional in Annotated params
|
|
69
72
|
]
|
|
70
73
|
|
|
71
74
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -102,3 +105,12 @@ pythonPlatform = "Linux"
|
|
|
102
105
|
venvPath = "."
|
|
103
106
|
venv = ".venv"
|
|
104
107
|
typeCheckingMode = "standard"
|
|
108
|
+
|
|
109
|
+
[tool.commitizen]
|
|
110
|
+
name = "cz_conventional_commits"
|
|
111
|
+
version_provider = "pep621"
|
|
112
|
+
tag_format = "v$version"
|
|
113
|
+
update_changelog_on_bump = true
|
|
114
|
+
changelog_file = "CHANGELOG.md"
|
|
115
|
+
changelog_incremental = true
|
|
116
|
+
bump_message = "bump: version $current_version → $new_version"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# pcp_mcp Core Package
|
|
2
|
+
|
|
3
|
+
## OVERVIEW
|
|
4
|
+
|
|
5
|
+
Core MCP server package. Entry point, HTTP client, configuration, models, error handling.
|
|
6
|
+
|
|
7
|
+
## STRUCTURE
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pcp_mcp/
|
|
11
|
+
├── __init__.py # CLI entry (main() with argparse)
|
|
12
|
+
├── server.py # FastMCP setup, lifespan context
|
|
13
|
+
├── client.py # PCPClient async httpx wrapper
|
|
14
|
+
├── config.py # PCPMCPSettings (Pydantic)
|
|
15
|
+
├── models.py # Response models (SystemSnapshot, ProcessInfo, etc.)
|
|
16
|
+
├── errors.py # Exception → ToolError mapping
|
|
17
|
+
├── context.py # get_client(), get_settings() helpers
|
|
18
|
+
├── middleware.py # Request caching middleware
|
|
19
|
+
├── icons.py # System assessment icons (emoji mappings)
|
|
20
|
+
├── tools/ # MCP tools (see tools/AGENTS.md)
|
|
21
|
+
├── resources/ # MCP resources (health.py, catalog.py)
|
|
22
|
+
├── utils/ # Extractors, builders
|
|
23
|
+
└── prompts/ # LLM system prompts
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## KEY PATTERNS
|
|
27
|
+
|
|
28
|
+
### Server Lifespan
|
|
29
|
+
```python
|
|
30
|
+
@asynccontextmanager
|
|
31
|
+
async def lifespan(mcp: FastMCP) -> AsyncIterator[dict]:
|
|
32
|
+
async with PCPClient(...) as client:
|
|
33
|
+
yield {"client": client, "settings": settings}
|
|
34
|
+
```
|
|
35
|
+
Tools access via `ctx.request_context.lifespan_context["client"]`.
|
|
36
|
+
|
|
37
|
+
### Client Rate Calculation
|
|
38
|
+
`fetch_with_rates()` takes two samples, calculates per-second rates for counters.
|
|
39
|
+
Handles counter wrap-around (reset to 0) gracefully.
|
|
40
|
+
|
|
41
|
+
### Error Mapping
|
|
42
|
+
```python
|
|
43
|
+
try:
|
|
44
|
+
result = await client.fetch(names)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
raise handle_pcp_error(e, "fetching metrics") from e
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Configuration
|
|
50
|
+
Pydantic settings with `env_prefix="PCP_"`. Computed properties: `base_url`, `auth`.
|
|
51
|
+
|
|
52
|
+
## WHERE TO LOOK
|
|
53
|
+
|
|
54
|
+
| Task | File | Notes |
|
|
55
|
+
|------|------|-------|
|
|
56
|
+
| Add CLI flag | `__init__.py` | argparse in `main()` |
|
|
57
|
+
| Change transport | `__init__.py` | `server.run(transport=...)` |
|
|
58
|
+
| Add env var | `config.py` | Add field with `Field(default=...)` |
|
|
59
|
+
| New response type | `models.py` | Inherit `BaseModel` |
|
|
60
|
+
| Map new exception | `errors.py` | Add case to `handle_pcp_error()` |
|
|
61
|
+
| Access client in tool | `context.py` | Use `get_client(ctx)` |
|
|
62
|
+
| Add caching | `middleware.py` | Request caching layer |
|
|
63
|
+
| System icons | `icons.py` | Assessment emoji mappings |
|
|
64
|
+
|
|
65
|
+
## ANTI-PATTERNS
|
|
66
|
+
|
|
67
|
+
- **NEVER** call `client.fetch()` for counter metrics expecting rates
|
|
68
|
+
- **NEVER** use client outside `async with` context
|
|
69
|
+
- **NEVER** log credentials from settings
|
|
70
|
+
- **ALWAYS** use `handle_pcp_error()` for exception wrapping
|
|
@@ -17,9 +17,13 @@ Environment Variables:
|
|
|
17
17
|
PCP_PORT pmproxy port (default: 44322)
|
|
18
18
|
PCP_TARGET_HOST Target pmcd host to monitor (default: localhost)
|
|
19
19
|
PCP_USE_TLS Use HTTPS for pmproxy connection (default: false)
|
|
20
|
+
PCP_TLS_VERIFY Verify TLS certificates (default: true)
|
|
21
|
+
PCP_TLS_CA_BUNDLE Path to custom CA bundle for TLS (optional)
|
|
20
22
|
PCP_TIMEOUT Request timeout in seconds (default: 30)
|
|
21
23
|
PCP_USERNAME HTTP basic auth user (optional)
|
|
22
24
|
PCP_PASSWORD HTTP basic auth password (optional)
|
|
25
|
+
PCP_ALLOWED_HOSTS Comma-separated hostspecs allowed via host parameter (optional)
|
|
26
|
+
If not set, only target_host is allowed. Use '*' for any host.
|
|
23
27
|
|
|
24
28
|
Examples:
|
|
25
29
|
# Monitor localhost (default)
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import sys
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
if sys.version_info >= (3, 11):
|
|
9
10
|
from typing import Self
|
|
@@ -12,6 +13,12 @@ else:
|
|
|
12
13
|
|
|
13
14
|
import httpx
|
|
14
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Callable, Coroutine
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
ProgressCallback = Callable[[float, float, str], Coroutine[Any, Any, None]]
|
|
21
|
+
|
|
15
22
|
|
|
16
23
|
class PCPClient:
|
|
17
24
|
"""Async client for pmproxy REST API.
|
|
@@ -24,6 +31,7 @@ class PCPClient:
|
|
|
24
31
|
target_host: Which pmcd host to connect to (passed as hostspec).
|
|
25
32
|
auth: Optional HTTP basic auth tuple (username, password).
|
|
26
33
|
timeout: Request timeout in seconds.
|
|
34
|
+
verify: TLS verification (True, False, or path to CA bundle).
|
|
27
35
|
"""
|
|
28
36
|
|
|
29
37
|
def __init__(
|
|
@@ -32,12 +40,14 @@ class PCPClient:
|
|
|
32
40
|
target_host: str = "localhost",
|
|
33
41
|
auth: tuple[str, str] | None = None,
|
|
34
42
|
timeout: float = 30.0,
|
|
43
|
+
verify: bool | str = True,
|
|
35
44
|
) -> None:
|
|
36
45
|
"""Initialize the PCP client."""
|
|
37
46
|
self._base_url = base_url
|
|
38
47
|
self._target_host = target_host
|
|
39
48
|
self._auth = auth
|
|
40
49
|
self._timeout = timeout
|
|
50
|
+
self._verify = verify
|
|
41
51
|
self._client: httpx.AsyncClient | None = None
|
|
42
52
|
self._context_id: int | None = None
|
|
43
53
|
|
|
@@ -47,6 +57,7 @@ class PCPClient:
|
|
|
47
57
|
base_url=self._base_url,
|
|
48
58
|
auth=self._auth,
|
|
49
59
|
timeout=self._timeout,
|
|
60
|
+
verify=self._verify,
|
|
50
61
|
)
|
|
51
62
|
resp = await self._client.get(
|
|
52
63
|
"/pmapi/context",
|
|
@@ -185,6 +196,7 @@ class PCPClient:
|
|
|
185
196
|
metric_names: list[str],
|
|
186
197
|
counter_metrics: set[str],
|
|
187
198
|
sample_interval: float = 1.0,
|
|
199
|
+
progress_callback: ProgressCallback | None = None,
|
|
188
200
|
) -> dict[str, dict]:
|
|
189
201
|
"""Fetch metrics, calculating rates for counters.
|
|
190
202
|
|
|
@@ -196,15 +208,31 @@ class PCPClient:
|
|
|
196
208
|
metric_names: List of PCP metric names to fetch.
|
|
197
209
|
counter_metrics: Set of metric names that are counters.
|
|
198
210
|
sample_interval: Seconds between samples for rate calculation.
|
|
211
|
+
progress_callback: Optional async callback for progress updates.
|
|
212
|
+
Called with (current, total, message) during long operations.
|
|
199
213
|
|
|
200
214
|
Returns:
|
|
201
215
|
Dict mapping metric name to {value, instances} where value/instances
|
|
202
216
|
contain the rate (for counters) or instant value (for gauges).
|
|
203
217
|
"""
|
|
218
|
+
if progress_callback:
|
|
219
|
+
await progress_callback(0, 100, "Collecting first sample...")
|
|
220
|
+
|
|
204
221
|
t1 = await self.fetch(metric_names)
|
|
222
|
+
|
|
223
|
+
if progress_callback:
|
|
224
|
+
await progress_callback(20, 100, f"Waiting {sample_interval}s for rate calculation...")
|
|
225
|
+
|
|
205
226
|
await asyncio.sleep(sample_interval)
|
|
227
|
+
|
|
228
|
+
if progress_callback:
|
|
229
|
+
await progress_callback(70, 100, "Collecting second sample...")
|
|
230
|
+
|
|
206
231
|
t2 = await self.fetch(metric_names)
|
|
207
232
|
|
|
233
|
+
if progress_callback:
|
|
234
|
+
await progress_callback(90, 100, "Computing rates...")
|
|
235
|
+
|
|
208
236
|
ts1 = t1.get("timestamp", 0.0)
|
|
209
237
|
ts2 = t2.get("timestamp", 0.0)
|
|
210
238
|
if isinstance(ts1, dict):
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Configuration for the PCP MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, computed_field
|
|
6
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PCPMCPSettings(BaseSettings):
|
|
10
|
+
"""Configuration for the PCP MCP server.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
host: pmproxy host.
|
|
14
|
+
port: pmproxy port.
|
|
15
|
+
use_tls: Use HTTPS for pmproxy connection.
|
|
16
|
+
timeout: Request timeout in seconds.
|
|
17
|
+
target_host: Target pmcd host to monitor (can be remote hostname).
|
|
18
|
+
username: HTTP basic auth user.
|
|
19
|
+
password: HTTP basic auth password.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
model_config = SettingsConfigDict(
|
|
23
|
+
env_file=".env",
|
|
24
|
+
env_prefix="PCP_",
|
|
25
|
+
extra="ignore",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
host: str = Field(default="localhost", description="pmproxy host")
|
|
29
|
+
port: int = Field(default=44322, description="pmproxy port")
|
|
30
|
+
use_tls: bool = Field(default=False, description="Use HTTPS for pmproxy connection")
|
|
31
|
+
tls_verify: bool = Field(
|
|
32
|
+
default=True,
|
|
33
|
+
description="Verify TLS certificates when use_tls is enabled",
|
|
34
|
+
)
|
|
35
|
+
tls_ca_bundle: str | None = Field(
|
|
36
|
+
default=None,
|
|
37
|
+
description="Path to custom CA bundle for TLS verification",
|
|
38
|
+
)
|
|
39
|
+
timeout: float = Field(default=30.0, description="Request timeout in seconds")
|
|
40
|
+
target_host: str = Field(
|
|
41
|
+
default="localhost",
|
|
42
|
+
description="Target pmcd host to monitor (can be remote hostname)",
|
|
43
|
+
)
|
|
44
|
+
username: str | None = Field(default=None, description="HTTP basic auth user")
|
|
45
|
+
password: str | None = Field(default=None, description="HTTP basic auth password")
|
|
46
|
+
allowed_hosts: list[str] | None = Field(
|
|
47
|
+
default=None,
|
|
48
|
+
description=(
|
|
49
|
+
"Allowlist of hostspecs that can be queried via the host parameter. "
|
|
50
|
+
"If None, only the configured target_host is allowed (default). "
|
|
51
|
+
"Set to ['*'] to allow any host (use with caution)."
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@computed_field
|
|
56
|
+
@property
|
|
57
|
+
def base_url(self) -> str:
|
|
58
|
+
"""URL for connecting to pmproxy."""
|
|
59
|
+
scheme = "https" if self.use_tls else "http"
|
|
60
|
+
return f"{scheme}://{self.host}:{self.port}"
|
|
61
|
+
|
|
62
|
+
@computed_field
|
|
63
|
+
@property
|
|
64
|
+
def auth(self) -> tuple[str, str] | None:
|
|
65
|
+
"""Auth tuple for httpx, or None if no auth configured."""
|
|
66
|
+
if self.username and self.password:
|
|
67
|
+
return (self.username, self.password)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
@computed_field
|
|
71
|
+
@property
|
|
72
|
+
def verify(self) -> bool | str:
|
|
73
|
+
"""TLS verification setting for httpx.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
False if verification disabled, path to CA bundle if specified,
|
|
77
|
+
or True for default system verification.
|
|
78
|
+
"""
|
|
79
|
+
if not self.tls_verify:
|
|
80
|
+
return False
|
|
81
|
+
if self.tls_ca_bundle:
|
|
82
|
+
return self.tls_ca_bundle
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
def is_host_allowed(self, host: str) -> bool:
|
|
86
|
+
"""Check if a host is allowed by the allowlist.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
host: The hostspec to validate.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True if the host is allowed, False otherwise.
|
|
93
|
+
"""
|
|
94
|
+
# Always allow the configured target_host
|
|
95
|
+
if host == self.target_host:
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
# If no allowlist configured, only target_host is allowed
|
|
99
|
+
if self.allowed_hosts is None:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# Wildcard allows everything
|
|
103
|
+
if "*" in self.allowed_hosts:
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
# Check exact match in allowlist
|
|
107
|
+
return host in self.allowed_hosts
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Context helpers for safe lifespan context access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from fastmcp import Context
|
|
10
|
+
from fastmcp.exceptions import ToolError
|
|
11
|
+
|
|
12
|
+
from pcp_mcp.client import PCPClient
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pcp_mcp.config import PCPMCPSettings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _validate_context(ctx: Context) -> None:
|
|
19
|
+
"""Validate context has lifespan_context available.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
ctx: MCP context.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ToolError: If context is not available.
|
|
26
|
+
"""
|
|
27
|
+
if ctx.request_context is None or ctx.request_context.lifespan_context is None:
|
|
28
|
+
raise ToolError("Server context not available")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_client(ctx: Context) -> PCPClient:
|
|
32
|
+
"""Get PCPClient from context.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
ctx: MCP context.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
The PCPClient instance.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ToolError: If context is not available.
|
|
42
|
+
"""
|
|
43
|
+
_validate_context(ctx)
|
|
44
|
+
assert ctx.request_context is not None
|
|
45
|
+
assert ctx.request_context.lifespan_context is not None
|
|
46
|
+
return ctx.request_context.lifespan_context["client"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_settings(ctx: Context) -> PCPMCPSettings:
|
|
50
|
+
"""Get settings from context.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
ctx: MCP context.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The PCPMCPSettings instance.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ToolError: If context is not available.
|
|
60
|
+
"""
|
|
61
|
+
_validate_context(ctx)
|
|
62
|
+
assert ctx.request_context is not None
|
|
63
|
+
assert ctx.request_context.lifespan_context is not None
|
|
64
|
+
return ctx.request_context.lifespan_context["settings"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@asynccontextmanager
|
|
68
|
+
async def get_client_for_host(ctx: Context, host: str | None = None) -> AsyncIterator[PCPClient]:
|
|
69
|
+
"""Get a PCPClient for the specified host.
|
|
70
|
+
|
|
71
|
+
If host is None or matches the configured target_host, yields the existing
|
|
72
|
+
lifespan client. Otherwise, creates a new ad-hoc client for the specified
|
|
73
|
+
hostspec and cleans it up on exit.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
ctx: MCP context.
|
|
77
|
+
host: Target pmcd hostspec to query. None uses the default.
|
|
78
|
+
|
|
79
|
+
Yields:
|
|
80
|
+
PCPClient connected to the specified host.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ToolError: If context is not available, host is not allowed, or host is unreachable.
|
|
84
|
+
"""
|
|
85
|
+
settings = get_settings(ctx)
|
|
86
|
+
|
|
87
|
+
if host is None or host == settings.target_host:
|
|
88
|
+
yield get_client(ctx)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if not settings.is_host_allowed(host):
|
|
92
|
+
raise ToolError(
|
|
93
|
+
f"Host '{host}' is not in the allowed hosts list. "
|
|
94
|
+
f"Configure PCP_ALLOWED_HOSTS to permit additional hosts."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
async with PCPClient(
|
|
98
|
+
base_url=settings.base_url,
|
|
99
|
+
target_host=host,
|
|
100
|
+
auth=settings.auth,
|
|
101
|
+
timeout=settings.timeout,
|
|
102
|
+
verify=settings.verify,
|
|
103
|
+
) as client:
|
|
104
|
+
yield client
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Centralized icons and tags for MCP components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from mcp.types import Icon
|
|
6
|
+
|
|
7
|
+
ICON_METRICS = Icon(src="data:,📊", mimeType="text/plain")
|
|
8
|
+
ICON_SEARCH = Icon(src="data:,🔍", mimeType="text/plain")
|
|
9
|
+
ICON_INFO = Icon(src="data:,📋", mimeType="text/plain")
|
|
10
|
+
ICON_SYSTEM = Icon(src="data:,💻", mimeType="text/plain")
|
|
11
|
+
ICON_PROCESS = Icon(src="data:,⚙️", mimeType="text/plain")
|
|
12
|
+
ICON_HEALTH = Icon(src="data:,💚", mimeType="text/plain")
|
|
13
|
+
ICON_CATALOG = Icon(src="data:,📚", mimeType="text/plain")
|
|
14
|
+
ICON_NAMESPACE = Icon(src="data:,🗂️", mimeType="text/plain")
|
|
15
|
+
ICON_DIAGNOSE = Icon(src="data:,🔬", mimeType="text/plain")
|
|
16
|
+
ICON_CPU = Icon(src="data:,🖥️", mimeType="text/plain")
|
|
17
|
+
ICON_MEMORY = Icon(src="data:,🧠", mimeType="text/plain")
|
|
18
|
+
ICON_DISK = Icon(src="data:,💾", mimeType="text/plain")
|
|
19
|
+
ICON_NETWORK = Icon(src="data:,🌐", mimeType="text/plain")
|
|
20
|
+
|
|
21
|
+
TAGS_METRICS = {"metrics", "pcp"}
|
|
22
|
+
TAGS_SYSTEM = {"system", "monitoring", "performance"}
|
|
23
|
+
TAGS_PROCESS = {"processes", "monitoring"}
|
|
24
|
+
TAGS_HEALTH = {"health", "status", "summary"}
|
|
25
|
+
TAGS_CATALOG = {"catalog", "reference", "documentation"}
|
|
26
|
+
TAGS_DISCOVERY = {"discovery", "namespace", "exploration"}
|
|
27
|
+
TAGS_CPU = {"cpu", "troubleshooting", "performance"}
|
|
28
|
+
TAGS_MEMORY = {"memory", "troubleshooting", "performance"}
|
|
29
|
+
TAGS_DISK = {"disk", "io", "troubleshooting", "performance"}
|
|
30
|
+
TAGS_NETWORK = {"network", "troubleshooting", "performance"}
|
|
31
|
+
TAGS_DIAGNOSE = {"diagnosis", "troubleshooting", "workflow"}
|