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.
@@ -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
- from pcp_mcp.prompts import register_prompts
130
- from pcp_mcp.tools import register_tools
131
-
132
- register_tools(mcp)
133
- register_prompts(mcp)
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 register_metrics_tools
18
- from pcp_mcp.tools.system import register_system_tools
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
- register_metrics_tools(mcp)
21
- register_system_tools(mcp)
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
- from typing import TYPE_CHECKING, Annotated, Optional
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
- def register_metrics_tools(mcp: "FastMCP") -> None:
33
- """Register core metric tools with the MCP server."""
34
-
35
- @mcp.tool(
36
- annotations=TOOL_ANNOTATIONS,
37
- icons=[ICON_METRICS],
38
- tags=TAGS_METRICS,
39
- )
40
- async def query_metrics(
41
- ctx: Context,
42
- names: Annotated[
43
- list[str],
44
- Field(description="List of PCP metric names to fetch (e.g., ['kernel.all.load'])"),
45
- ],
46
- host: Annotated[
47
- Optional[str],
48
- Field(description="Target pmcd host to query (default: server's configured target)"),
49
- ] = None,
50
- ) -> MetricValueList:
51
- """Fetch current values for specific PCP metrics.
52
-
53
- Returns the current value for each requested metric. For metrics with
54
- instances (e.g., per-CPU, per-disk), returns one MetricValue per instance.
55
-
56
- Examples:
57
- query_metrics(["kernel.all.load"]) - Get load averages
58
- query_metrics(["mem.util.available", "mem.physmem"]) - Get memory stats
59
- query_metrics(["hinv.ncpu"]) - Get CPU count
60
- query_metrics(["kernel.all.load"], host="web1.example.com") - Query remote host
61
-
62
- Warning: CPU, disk, and network metrics are counters (cumulative since boot).
63
- Use get_system_snapshot() instead for rates.
64
- """
65
- from pcp_mcp.errors import handle_pcp_error
66
-
67
- async with get_client_for_host(ctx, host) as client:
68
- try:
69
- response = await client.fetch(names)
70
- except Exception as e:
71
- raise handle_pcp_error(e, "fetching metrics") from e
72
-
73
- results: list[MetricValue] = []
74
- for metric in response.get("values", []):
75
- metric_name = metric.get("name", "")
76
- instances = metric.get("instances", [])
77
-
78
- for inst in instances:
79
- instance_id = inst.get("instance")
80
- value = inst.get("value")
81
-
82
- instance_name = None
83
- if instance_id is not None and instance_id != -1:
84
- instance_name = str(instance_id)
85
-
86
- results.append(
87
- MetricValue(
88
- name=metric_name,
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
- for m in metrics
138
- ]
139
- return MetricSearchResultList(results=results)
140
-
141
- @mcp.tool(
142
- annotations=TOOL_ANNOTATIONS,
143
- output_schema=MetricInfo.model_json_schema(),
144
- icons=[ICON_INFO],
145
- tags=TAGS_METRICS | TAGS_DISCOVERY,
146
- )
147
- async def describe_metric(
148
- ctx: Context,
149
- name: Annotated[
150
- str,
151
- Field(description="Full PCP metric name (e.g., 'kernel.all.cpu.user')"),
152
- ],
153
- host: Annotated[
154
- Optional[str],
155
- Field(description="Target pmcd host to query (default: server's configured target)"),
156
- ] = None,
157
- ) -> MetricInfo:
158
- """Get detailed metadata about a PCP metric.
159
-
160
- Returns type, semantics, units, and help text for the metric.
161
- Use this to understand what a metric measures and how to interpret it.
162
-
163
- Examples:
164
- describe_metric("kernel.all.load") - Learn about load average semantics
165
- describe_metric("mem.util.available") - Understand available memory
166
- describe_metric("disk.all.read_bytes") - Check if metric is counter vs instant
167
- describe_metric("kernel.all.load", host="web1.example.com") - Describe on remote
168
- """
169
- from fastmcp.exceptions import ToolError
170
-
171
- from pcp_mcp.errors import handle_pcp_error
172
-
173
- async with get_client_for_host(ctx, host) as client:
174
- try:
175
- info = await client.describe(name)
176
- except Exception as e:
177
- raise handle_pcp_error(e, "describing metric") from e
178
-
179
- if not info:
180
- raise ToolError(f"Metric not found: {name}")
181
-
182
- return MetricInfo(
183
- name=info.get("name", name),
184
- type=info.get("type", "unknown"),
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
+ )