pcp-mcp 1.3.2__py3-none-any.whl → 1.4.0__py3-none-any.whl
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/errors.py +23 -16
- pcp_mcp/prompts/__init__.py +20 -292
- pcp_mcp/prompts/cpu.py +69 -0
- pcp_mcp/prompts/diagnose.py +54 -0
- pcp_mcp/prompts/disk.py +69 -0
- pcp_mcp/prompts/memory.py +60 -0
- pcp_mcp/prompts/network.py +67 -0
- pcp_mcp/server.py +9 -5
- pcp_mcp/tools/__init__.py +20 -4
- pcp_mcp/tools/metrics.py +174 -160
- pcp_mcp/tools/system.py +293 -262
- {pcp_mcp-1.3.2.dist-info → pcp_mcp-1.4.0.dist-info}/METADATA +2 -2
- pcp_mcp-1.4.0.dist-info/RECORD +28 -0
- {pcp_mcp-1.3.2.dist-info → pcp_mcp-1.4.0.dist-info}/WHEEL +1 -1
- pcp_mcp-1.3.2.dist-info/RECORD +0 -23
- {pcp_mcp-1.3.2.dist-info → pcp_mcp-1.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Check network performance prompt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastmcp.prompts import prompt
|
|
6
|
+
|
|
7
|
+
from pcp_mcp.icons import ICON_NETWORK, TAGS_NETWORK
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@prompt(icons=[ICON_NETWORK], tags=TAGS_NETWORK)
|
|
11
|
+
def check_network_performance() -> str:
|
|
12
|
+
"""Check network performance and identify bandwidth/error issues.
|
|
13
|
+
|
|
14
|
+
Returns a workflow to analyze network throughput, identify saturated
|
|
15
|
+
interfaces, and detect packet loss or errors.
|
|
16
|
+
"""
|
|
17
|
+
return """Network performance investigation:
|
|
18
|
+
|
|
19
|
+
1. Get network overview:
|
|
20
|
+
- Run: get_system_snapshot(categories=["network"])
|
|
21
|
+
- Read network.assessment for quick diagnosis
|
|
22
|
+
- Note: Rates are per-second (bytes/sec, packets/sec)
|
|
23
|
+
|
|
24
|
+
2. Interpret network metrics:
|
|
25
|
+
- in_bytes_sec / out_bytes_sec: Throughput (compare to link speed)
|
|
26
|
+
- in_packets_sec / out_packets_sec: Packet rate
|
|
27
|
+
- Assessment field indicates saturation or errors
|
|
28
|
+
|
|
29
|
+
3. Per-interface breakdown:
|
|
30
|
+
- Run: search_metrics("network.interface")
|
|
31
|
+
- Run: query_metrics(["network.interface.in.bytes", "network.interface.out.bytes"])
|
|
32
|
+
- Note: These are COUNTERS, use get_system_snapshot for rates
|
|
33
|
+
- Identify busy interfaces vs idle interfaces (e.g., eth0 busy, lo idle)
|
|
34
|
+
|
|
35
|
+
4. Check for errors and drops:
|
|
36
|
+
- Run: query_metrics(["network.interface.in.errors", "network.interface.out.errors"])
|
|
37
|
+
- Run: query_metrics(["network.interface.in.drops", "network.interface.out.drops"])
|
|
38
|
+
- Non-zero errors = Hardware, driver, or cable issues
|
|
39
|
+
- Non-zero drops = Buffer overflow (traffic exceeds processing capacity)
|
|
40
|
+
|
|
41
|
+
5. Calculate interface saturation:
|
|
42
|
+
- Compare throughput to link speed (e.g., 950 Mbps on 1 Gbps link = 95%)
|
|
43
|
+
- Sustained >80% = Approaching saturation
|
|
44
|
+
- Bursts >95% = Temporarily saturated
|
|
45
|
+
|
|
46
|
+
6. Find network-heavy processes (indirect):
|
|
47
|
+
- PCP proc.* namespace doesn't have per-process network metrics
|
|
48
|
+
- Use system tools: netstat, ss, iftop (outside PCP)
|
|
49
|
+
- Or correlate: High network I/O often correlates with high CPU/disk I/O
|
|
50
|
+
|
|
51
|
+
7. Check protocol-level stats (if needed):
|
|
52
|
+
- Run: search_metrics("network.tcp")
|
|
53
|
+
- Run: search_metrics("network.udp")
|
|
54
|
+
- Look for: Retransmissions, failed connections, buffer overflows
|
|
55
|
+
|
|
56
|
+
8. Report:
|
|
57
|
+
- Per-interface throughput (e.g., "eth0: 850 Mbps in, 120 Mbps out")
|
|
58
|
+
- Link utilization % (if link speed known)
|
|
59
|
+
- Errors/drops: Count and affected interfaces
|
|
60
|
+
- Traffic pattern: Symmetric (similar in/out) vs asymmetric (download/upload heavy)
|
|
61
|
+
- Packet rate: Normal vs abnormal (tiny packets = inefficient, possible attack)
|
|
62
|
+
- Recommendations:
|
|
63
|
+
* High utilization + no errors → Upgrade link or load balance
|
|
64
|
+
* Errors/drops present → Check cables, NIC drivers, switch ports
|
|
65
|
+
* Asymmetric traffic → Normal for client (download heavy) or server (upload heavy)
|
|
66
|
+
* High packet rate + low byte rate → Small packets (check for SYN flood, fragmentation)
|
|
67
|
+
"""
|
pcp_mcp/server.py
CHANGED
|
@@ -4,10 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from collections.abc import AsyncIterator
|
|
6
6
|
from contextlib import asynccontextmanager
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
from fastmcp import FastMCP
|
|
10
11
|
from fastmcp.server.middleware.logging import StructuredLoggingMiddleware
|
|
12
|
+
from fastmcp.server.providers import FileSystemProvider
|
|
11
13
|
|
|
12
14
|
from pcp_mcp.client import PCPClient
|
|
13
15
|
from pcp_mcp.config import PCPMCPSettings
|
|
@@ -126,10 +128,12 @@ Prompts (invoke for guided troubleshooting workflows):
|
|
|
126
128
|
)
|
|
127
129
|
mcp.add_middleware(MetricCacheMiddleware())
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
# Auto-discover tools and prompts from filesystem
|
|
132
|
+
base_dir = Path(__file__).parent
|
|
133
|
+
provider = FileSystemProvider(
|
|
134
|
+
root=base_dir,
|
|
135
|
+
reload=False,
|
|
136
|
+
)
|
|
137
|
+
mcp.add_provider(provider)
|
|
134
138
|
|
|
135
139
|
return mcp
|
pcp_mcp/tools/__init__.py
CHANGED
|
@@ -14,8 +14,24 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
14
14
|
Args:
|
|
15
15
|
mcp: The FastMCP server instance.
|
|
16
16
|
"""
|
|
17
|
-
from pcp_mcp.tools.metrics import
|
|
18
|
-
|
|
17
|
+
from pcp_mcp.tools.metrics import (
|
|
18
|
+
describe_metric,
|
|
19
|
+
query_metrics,
|
|
20
|
+
search_metrics,
|
|
21
|
+
)
|
|
22
|
+
from pcp_mcp.tools.system import (
|
|
23
|
+
get_filesystem_usage,
|
|
24
|
+
get_process_top,
|
|
25
|
+
get_system_snapshot,
|
|
26
|
+
quick_health,
|
|
27
|
+
smart_diagnose,
|
|
28
|
+
)
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
mcp.add_tool(query_metrics)
|
|
31
|
+
mcp.add_tool(search_metrics)
|
|
32
|
+
mcp.add_tool(describe_metric)
|
|
33
|
+
mcp.add_tool(get_system_snapshot)
|
|
34
|
+
mcp.add_tool(quick_health)
|
|
35
|
+
mcp.add_tool(get_process_top)
|
|
36
|
+
mcp.add_tool(smart_diagnose)
|
|
37
|
+
mcp.add_tool(get_filesystem_usage)
|
pcp_mcp/tools/metrics.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"""Core metric tools for querying PCP metrics."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import json
|
|
4
|
+
from typing import Annotated, Optional
|
|
4
5
|
|
|
5
6
|
from fastmcp import Context
|
|
7
|
+
from fastmcp.tools import tool
|
|
8
|
+
from fastmcp.tools.tool import ToolResult
|
|
6
9
|
from mcp.types import ToolAnnotations
|
|
7
10
|
from pydantic import Field
|
|
8
11
|
|
|
12
|
+
__all__ = ["query_metrics", "search_metrics", "describe_metric"]
|
|
13
|
+
|
|
9
14
|
from pcp_mcp.context import get_client_for_host
|
|
10
15
|
from pcp_mcp.icons import (
|
|
11
16
|
ICON_INFO,
|
|
@@ -19,171 +24,180 @@ from pcp_mcp.models import (
|
|
|
19
24
|
MetricSearchResult,
|
|
20
25
|
MetricSearchResultList,
|
|
21
26
|
MetricValue,
|
|
22
|
-
MetricValueList,
|
|
23
27
|
)
|
|
24
28
|
from pcp_mcp.utils.extractors import extract_help_text, format_units
|
|
25
29
|
|
|
26
|
-
if TYPE_CHECKING:
|
|
27
|
-
from fastmcp import FastMCP
|
|
28
|
-
|
|
29
30
|
TOOL_ANNOTATIONS = ToolAnnotations(readOnlyHint=True, openWorldHint=True)
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
names
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
],
|
|
46
|
-
host:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
instance_name =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
value=value,
|
|
90
|
-
instance=instance_name,
|
|
91
|
-
)
|
|
33
|
+
@tool(
|
|
34
|
+
annotations=TOOL_ANNOTATIONS,
|
|
35
|
+
icons=[ICON_METRICS],
|
|
36
|
+
tags=TAGS_METRICS,
|
|
37
|
+
timeout=30.0,
|
|
38
|
+
)
|
|
39
|
+
async def query_metrics(
|
|
40
|
+
ctx: Context,
|
|
41
|
+
names: Annotated[
|
|
42
|
+
list[str],
|
|
43
|
+
Field(description="List of PCP metric names to fetch (e.g., ['kernel.all.load'])"),
|
|
44
|
+
],
|
|
45
|
+
host: Annotated[
|
|
46
|
+
Optional[str],
|
|
47
|
+
Field(description="Target pmcd host to query (default: server's configured target)"),
|
|
48
|
+
] = None,
|
|
49
|
+
) -> ToolResult:
|
|
50
|
+
"""Fetch current values for specific PCP metrics.
|
|
51
|
+
|
|
52
|
+
Returns the current value for each requested metric. For metrics with
|
|
53
|
+
instances (e.g., per-CPU, per-disk), returns one MetricValue per instance.
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
query_metrics(["kernel.all.load"]) - Get load averages
|
|
57
|
+
query_metrics(["mem.util.available", "mem.physmem"]) - Get memory stats
|
|
58
|
+
query_metrics(["hinv.ncpu"]) - Get CPU count
|
|
59
|
+
query_metrics(["kernel.all.load"], host="web1.example.com") - Query remote host
|
|
60
|
+
|
|
61
|
+
Warning: CPU, disk, and network metrics are counters (cumulative since boot).
|
|
62
|
+
Use get_system_snapshot() instead for rates.
|
|
63
|
+
"""
|
|
64
|
+
from pcp_mcp.errors import handle_pcp_error
|
|
65
|
+
|
|
66
|
+
async with get_client_for_host(ctx, host) as client:
|
|
67
|
+
try:
|
|
68
|
+
response = await client.fetch(names)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise handle_pcp_error(e, "fetching metrics") from e
|
|
71
|
+
|
|
72
|
+
results: list[MetricValue] = []
|
|
73
|
+
for metric in response.get("values", []):
|
|
74
|
+
metric_name = metric.get("name", "")
|
|
75
|
+
instances = metric.get("instances", [])
|
|
76
|
+
|
|
77
|
+
for inst in instances:
|
|
78
|
+
instance_id = inst.get("instance")
|
|
79
|
+
value = inst.get("value")
|
|
80
|
+
|
|
81
|
+
instance_name = None
|
|
82
|
+
if instance_id is not None and instance_id != -1:
|
|
83
|
+
instance_name = str(instance_id)
|
|
84
|
+
|
|
85
|
+
results.append(
|
|
86
|
+
MetricValue(
|
|
87
|
+
name=metric_name,
|
|
88
|
+
value=value,
|
|
89
|
+
instance=instance_name,
|
|
92
90
|
)
|
|
93
|
-
|
|
94
|
-
return MetricValueList(metrics=results)
|
|
95
|
-
|
|
96
|
-
@mcp.tool(
|
|
97
|
-
annotations=TOOL_ANNOTATIONS,
|
|
98
|
-
icons=[ICON_SEARCH],
|
|
99
|
-
tags=TAGS_METRICS | TAGS_DISCOVERY,
|
|
100
|
-
)
|
|
101
|
-
async def search_metrics(
|
|
102
|
-
ctx: Context,
|
|
103
|
-
pattern: Annotated[
|
|
104
|
-
str,
|
|
105
|
-
Field(description="Metric name prefix to search for (e.g., 'kernel.all', 'mem')"),
|
|
106
|
-
],
|
|
107
|
-
host: Annotated[
|
|
108
|
-
Optional[str],
|
|
109
|
-
Field(description="Target pmcd host to query (default: server's configured target)"),
|
|
110
|
-
] = None,
|
|
111
|
-
) -> MetricSearchResultList:
|
|
112
|
-
"""Find PCP metrics matching a name pattern.
|
|
113
|
-
|
|
114
|
-
Use this to discover available metrics before querying them.
|
|
115
|
-
Returns metric names and brief descriptions.
|
|
116
|
-
|
|
117
|
-
Examples:
|
|
118
|
-
search_metrics("kernel.all") - Find kernel-wide metrics
|
|
119
|
-
search_metrics("mem.util") - Find memory utilization metrics
|
|
120
|
-
search_metrics("disk.dev") - Find per-disk metrics
|
|
121
|
-
search_metrics("network.interface") - Find per-interface metrics
|
|
122
|
-
search_metrics("kernel", host="db1.example.com") - Search on remote host
|
|
123
|
-
"""
|
|
124
|
-
from pcp_mcp.errors import handle_pcp_error
|
|
125
|
-
|
|
126
|
-
async with get_client_for_host(ctx, host) as client:
|
|
127
|
-
try:
|
|
128
|
-
metrics = await client.search(pattern)
|
|
129
|
-
except Exception as e:
|
|
130
|
-
raise handle_pcp_error(e, "searching metrics") from e
|
|
131
|
-
|
|
132
|
-
results = [
|
|
133
|
-
MetricSearchResult(
|
|
134
|
-
name=m.get("name", ""),
|
|
135
|
-
help_text=extract_help_text(m),
|
|
136
91
|
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
""
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
name=
|
|
184
|
-
|
|
185
|
-
semantics=info.get("sem", "unknown"),
|
|
186
|
-
units=format_units(info),
|
|
187
|
-
help_text=extract_help_text(info),
|
|
188
|
-
indom=info.get("indom"),
|
|
92
|
+
|
|
93
|
+
return ToolResult(
|
|
94
|
+
content=json.dumps([v.model_dump() for v in results]),
|
|
95
|
+
structured_content={"metrics": [v.model_dump() for v in results]},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@tool(
|
|
100
|
+
annotations=TOOL_ANNOTATIONS,
|
|
101
|
+
icons=[ICON_SEARCH],
|
|
102
|
+
tags=TAGS_METRICS | TAGS_DISCOVERY,
|
|
103
|
+
timeout=30.0,
|
|
104
|
+
)
|
|
105
|
+
async def search_metrics(
|
|
106
|
+
ctx: Context,
|
|
107
|
+
pattern: Annotated[
|
|
108
|
+
str,
|
|
109
|
+
Field(description="Metric name prefix to search for (e.g., 'kernel.all', 'mem')"),
|
|
110
|
+
],
|
|
111
|
+
host: Annotated[
|
|
112
|
+
Optional[str],
|
|
113
|
+
Field(description="Target pmcd host to query (default: server's configured target)"),
|
|
114
|
+
] = None,
|
|
115
|
+
) -> ToolResult:
|
|
116
|
+
"""Find PCP metrics matching a name pattern.
|
|
117
|
+
|
|
118
|
+
Use this to discover available metrics before querying them.
|
|
119
|
+
Returns metric names and brief descriptions.
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
search_metrics("kernel.all") - Find kernel-wide metrics
|
|
123
|
+
search_metrics("mem.util") - Find memory utilization metrics
|
|
124
|
+
search_metrics("disk.dev") - Find per-disk metrics
|
|
125
|
+
search_metrics("network.interface") - Find per-interface metrics
|
|
126
|
+
search_metrics("kernel", host="db1.example.com") - Search on remote host
|
|
127
|
+
"""
|
|
128
|
+
from pcp_mcp.errors import handle_pcp_error
|
|
129
|
+
|
|
130
|
+
async with get_client_for_host(ctx, host) as client:
|
|
131
|
+
try:
|
|
132
|
+
metrics = await client.search(pattern)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
raise handle_pcp_error(e, "searching metrics") from e
|
|
135
|
+
|
|
136
|
+
results = [
|
|
137
|
+
MetricSearchResult(
|
|
138
|
+
name=m.get("name", ""),
|
|
139
|
+
help_text=extract_help_text(m),
|
|
189
140
|
)
|
|
141
|
+
for m in metrics
|
|
142
|
+
]
|
|
143
|
+
result = MetricSearchResultList(results=results)
|
|
144
|
+
return ToolResult(
|
|
145
|
+
content=result.model_dump_json(),
|
|
146
|
+
structured_content=result.model_dump(),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@tool(
|
|
151
|
+
annotations=TOOL_ANNOTATIONS,
|
|
152
|
+
output_schema=MetricInfo.model_json_schema(),
|
|
153
|
+
icons=[ICON_INFO],
|
|
154
|
+
tags=TAGS_METRICS | TAGS_DISCOVERY,
|
|
155
|
+
timeout=30.0,
|
|
156
|
+
)
|
|
157
|
+
async def describe_metric(
|
|
158
|
+
ctx: Context,
|
|
159
|
+
name: Annotated[
|
|
160
|
+
str,
|
|
161
|
+
Field(description="Full PCP metric name (e.g., 'kernel.all.cpu.user')"),
|
|
162
|
+
],
|
|
163
|
+
host: Annotated[
|
|
164
|
+
Optional[str],
|
|
165
|
+
Field(description="Target pmcd host to query (default: server's configured target)"),
|
|
166
|
+
] = None,
|
|
167
|
+
) -> ToolResult:
|
|
168
|
+
"""Get detailed metadata about a PCP metric.
|
|
169
|
+
|
|
170
|
+
Returns type, semantics, units, and help text for the metric.
|
|
171
|
+
Use this to understand what a metric measures and how to interpret it.
|
|
172
|
+
|
|
173
|
+
Examples:
|
|
174
|
+
describe_metric("kernel.all.load") - Learn about load average semantics
|
|
175
|
+
describe_metric("mem.util.available") - Understand available memory
|
|
176
|
+
describe_metric("disk.all.read_bytes") - Check if metric is counter vs instant
|
|
177
|
+
describe_metric("kernel.all.load", host="web1.example.com") - Describe on remote
|
|
178
|
+
"""
|
|
179
|
+
from fastmcp.exceptions import ToolError
|
|
180
|
+
|
|
181
|
+
from pcp_mcp.errors import handle_pcp_error
|
|
182
|
+
|
|
183
|
+
async with get_client_for_host(ctx, host) as client:
|
|
184
|
+
try:
|
|
185
|
+
info = await client.describe(name)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
raise handle_pcp_error(e, "describing metric") from e
|
|
188
|
+
|
|
189
|
+
if not info:
|
|
190
|
+
raise ToolError(f"Metric not found: {name}")
|
|
191
|
+
|
|
192
|
+
result = MetricInfo(
|
|
193
|
+
name=info.get("name", name),
|
|
194
|
+
type=info.get("type", "unknown"),
|
|
195
|
+
semantics=info.get("sem", "unknown"),
|
|
196
|
+
units=format_units(info),
|
|
197
|
+
help_text=extract_help_text(info),
|
|
198
|
+
indom=info.get("indom"),
|
|
199
|
+
)
|
|
200
|
+
return ToolResult(
|
|
201
|
+
content=result.model_dump_json(),
|
|
202
|
+
structured_content=result.model_dump(),
|
|
203
|
+
)
|