orb-cloud-mcp 0.1.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,3 @@
1
+ """MCP server for Orb Cloud device management."""
2
+
3
+ __version__ = "0.1.0"
orb_cloud_mcp/cache.py ADDED
@@ -0,0 +1,25 @@
1
+ """Shared TTL cache for stable Orb Cloud API responses."""
2
+
3
+ import os
4
+
5
+ from cachetools import TTLCache
6
+
7
+ _DEFAULT_TTL = 300 # seconds
8
+
9
+
10
+ def _ttl() -> int:
11
+ return int(os.environ.get("ORB_CLOUD_CACHE_TTL", _DEFAULT_TTL))
12
+
13
+
14
+ # Module-level cache shared across tools. Sized generously; in practice only a
15
+ # handful of organizations and their device lists will ever be stored.
16
+ cache: TTLCache = TTLCache(maxsize=256, ttl=_ttl())
17
+
18
+
19
+ def cache_key(*args: object) -> tuple:
20
+ """Build a cache key scoped to the current API token.
21
+
22
+ The token is hashed so it is never stored in plaintext as a dict key,
23
+ preventing accidental exposure in logs or debug output.
24
+ """
25
+ return (hash(os.environ.get("ORB_CLOUD_API_KEY")), *args)
@@ -0,0 +1,20 @@
1
+ """Orb Cloud client factory, reads credentials from environment."""
2
+
3
+ import os
4
+
5
+ from orb_cloud_client.client import OrbCloudClientAsync
6
+
7
+
8
+ def get_client() -> OrbCloudClientAsync:
9
+ """Return an authenticated async Orb Cloud client.
10
+
11
+ Raises:
12
+ RuntimeError: If ORB_CLOUD_API_KEY is not set.
13
+ """
14
+ token = os.environ.get("ORB_CLOUD_API_KEY")
15
+ if not token:
16
+ raise RuntimeError(
17
+ "ORB_CLOUD_API_KEY environment variable is not set. "
18
+ "Set it to your Orb Cloud API token."
19
+ )
20
+ return OrbCloudClientAsync(token=token)
orb_cloud_mcp/py.typed ADDED
File without changes
@@ -0,0 +1,43 @@
1
+ """FastMCP server for Orb Cloud."""
2
+
3
+ from fastmcp import FastMCP
4
+
5
+ from orb_cloud_mcp.tools.organizations import list_organizations
6
+ from orb_cloud_mcp.tools.devices import list_devices, get_device_telemetry
7
+ from orb_cloud_mcp.tools.speedtest import trigger_speedtest
8
+ from orb_cloud_mcp.tools.datasets import configure_temp_datasets
9
+
10
+ mcp = FastMCP(
11
+ name="orb-cloud",
12
+ instructions="Manage Orb Cloud devices, organizations, and run diagnostics.",
13
+ )
14
+
15
+ # --- Tools ---
16
+
17
+ mcp.tool()(list_organizations)
18
+ mcp.tool()(list_devices)
19
+ mcp.tool()(get_device_telemetry)
20
+ mcp.tool()(trigger_speedtest)
21
+ mcp.tool()(configure_temp_datasets)
22
+
23
+ # --- Resources ---
24
+
25
+
26
+ @mcp.resource("orb://organizations")
27
+ async def resource_organizations() -> list:
28
+ """All Orb Cloud organizations accessible with the configured API key."""
29
+ return await list_organizations()
30
+
31
+
32
+ @mcp.resource("orb://organizations/{organization_id}/devices")
33
+ async def resource_devices(organization_id: str) -> list:
34
+ """Stable configuration details for all devices in an Orb Cloud organization."""
35
+ return await list_devices(organization_id)
36
+
37
+
38
+ def main() -> None:
39
+ mcp.run()
40
+
41
+
42
+ if __name__ == "__main__":
43
+ main()
File without changes
@@ -0,0 +1,108 @@
1
+ """MCP tool to configure temporary dataset push on Orb Cloud devices."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from orb_cloud_mcp.client import get_client
8
+
9
+
10
+ class DataPushConfig(BaseModel):
11
+ """Configuration for pushing data to a custom HTTP endpoint."""
12
+
13
+ url: str = Field(..., description="Destination URL to push data to.")
14
+ datasets: list[str] | None = Field(
15
+ None,
16
+ description="Dataset names to include (e.g. ['responsiveness_1s', 'speed_results']).",
17
+ )
18
+ enabled: bool | None = Field(None, description="Whether pushing is enabled.")
19
+ identifiable: bool | None = Field(
20
+ None, description="Whether to include identifiable data."
21
+ )
22
+ format: str | None = Field(None, description="Payload format (e.g. 'json').")
23
+ interval_ms: int | None = Field(None, description="Push interval in milliseconds.")
24
+ buffer_kb: int | None = Field(None, description="Buffer size in kilobytes.")
25
+
26
+
27
+ class DataAPIConfig(BaseModel):
28
+ """Configuration for exposing a local data API on the device."""
29
+
30
+ enabled: bool | None = Field(None, description="Whether the data API is enabled.")
31
+ datasets: list[str] | None = Field(
32
+ None, description="Dataset names to expose via the API."
33
+ )
34
+ identifiable: bool | None = Field(
35
+ None, description="Whether to include identifiable data."
36
+ )
37
+ api_key: str | None = Field(None, description="API key for the local data API.")
38
+ port: int | None = Field(None, description="Port for the local data API.")
39
+ buffer: int | None = Field(None, description="Buffer size.")
40
+
41
+
42
+ class DatasetsConfig(BaseModel):
43
+ """Top-level dataset configuration."""
44
+
45
+ datasets: list[str] | None = Field(None, description="Dataset names to collect.")
46
+ enabled: bool | None = Field(
47
+ None, description="Whether data collection is enabled."
48
+ )
49
+ push: DataPushConfig | None = Field(
50
+ None, description="Push data to a custom HTTP endpoint."
51
+ )
52
+ cloud_push: DataPushConfig | None = Field(
53
+ None, description="Push data to Orb Cloud."
54
+ )
55
+ api: DataAPIConfig | None = Field(
56
+ None, description="Expose data via a local API endpoint on the device."
57
+ )
58
+
59
+
60
+ async def configure_temp_datasets(
61
+ device_id: str,
62
+ duration: str = Field(
63
+ ..., description="How long to enable (e.g. '30m', '1h', '2h')."
64
+ ),
65
+ datasets_config: DatasetsConfig | None = None,
66
+ ) -> dict[str, Any]:
67
+ """Configure temporary dataset collection and push for an Orb Cloud device.
68
+
69
+ Enables the device to push data to a custom endpoint or expose a local API
70
+ for a fixed duration. After the duration expires, the device reverts to its
71
+ default configuration.
72
+
73
+ Args:
74
+ device_id: The Orb device ID to configure.
75
+ duration: How long to enable the configuration (e.g. '30m', '1h').
76
+ datasets_config: Dataset collection and push configuration. If omitted,
77
+ only the duration is updated.
78
+ """
79
+ from orb_cloud_client.models.generic import TempDatasetsRequest
80
+ from orb_cloud_client.models.config import Datasets, DataPush, DataAPI
81
+
82
+ def _to_data_push(cfg: DataPushConfig | None) -> DataPush | None:
83
+ if cfg is None:
84
+ return None
85
+ return DataPush(**cfg.model_dump(exclude_none=True))
86
+
87
+ def _to_data_api(cfg: DataAPIConfig | None) -> DataAPI | None:
88
+ if cfg is None:
89
+ return None
90
+ return DataAPI(**cfg.model_dump(exclude_none=True))
91
+
92
+ sdk_datasets: Datasets | None = None
93
+ if datasets_config is not None:
94
+ sdk_datasets = Datasets(
95
+ datasets=datasets_config.datasets,
96
+ enabled=datasets_config.enabled,
97
+ push=_to_data_push(datasets_config.push),
98
+ cloud_push=_to_data_push(datasets_config.cloud_push),
99
+ api=_to_data_api(datasets_config.api),
100
+ )
101
+
102
+ request = TempDatasetsRequest(
103
+ duration=duration,
104
+ datasets_config=sdk_datasets,
105
+ )
106
+
107
+ async with get_client() as client:
108
+ return await client.configure_temporary_datasets(device_id, request)
@@ -0,0 +1,138 @@
1
+ """MCP tools for Orb Cloud devices."""
2
+
3
+ from typing import Any
4
+
5
+ from orb_cloud_client.models.generic import Device, OrbScore
6
+
7
+ from orb_cloud_mcp.cache import cache, cache_key
8
+ from orb_cloud_mcp.client import get_client
9
+
10
+ _CACHE_KEY = "list_devices"
11
+
12
+
13
+ async def list_devices(organization_id: str) -> list[dict[str, Any]]:
14
+ """List devices in an Orb Cloud organization with stable configuration details.
15
+
16
+ Returns device identity, hardware info, location, and configuration — data
17
+ that changes rarely. For real-time connectivity and performance scores, use
18
+ get_device_telemetry instead.
19
+
20
+ Args:
21
+ organization_id: The organization ID to list devices for.
22
+ """
23
+ key = cache_key(_CACHE_KEY, organization_id)
24
+ if key in cache:
25
+ return cache[key]
26
+
27
+ async with get_client() as client:
28
+ devices = await client.get_organization_devices(organization_id)
29
+ result = [_serialize_stable(d) for d in devices]
30
+ cache[key] = result
31
+ return result
32
+
33
+
34
+ async def get_device_telemetry(
35
+ organization_id: str,
36
+ device_id: str | None = None,
37
+ ) -> list[dict[str, Any]]:
38
+ """Get live telemetry for devices in an Orb Cloud organization.
39
+
40
+ Returns real-time connectivity status and Orb performance scores. If
41
+ device_id is provided, returns telemetry for that device only.
42
+
43
+ Args:
44
+ organization_id: The organization ID to query devices for.
45
+ device_id: Optional Orb device ID to filter to a single device.
46
+ """
47
+ async with get_client() as client:
48
+ devices = await client.get_organization_devices(organization_id)
49
+
50
+ if device_id is not None:
51
+ devices = [d for d in devices if d.orb_id == device_id]
52
+
53
+ return [_serialize_telemetry(d) for d in devices]
54
+
55
+
56
+ def _serialize_score(score: OrbScore | None) -> dict[str, Any] | None:
57
+ if score is None:
58
+ return None
59
+ result: dict[str, Any] = {
60
+ "score": score.score,
61
+ "display": score.display,
62
+ "included": score.included,
63
+ "value": score.value,
64
+ "duration_ms": score.duration_ms,
65
+ "score_version": score.score_version,
66
+ }
67
+ if score.components:
68
+ result["components"] = {
69
+ k: _serialize_score(v) for k, v in score.components.items()
70
+ }
71
+ return result
72
+
73
+
74
+ def _serialize_stable(device: Device) -> dict[str, Any]:
75
+ """Serialize the stable, slowly-changing fields of a device."""
76
+ info: dict[str, Any] = {}
77
+ if device.summary:
78
+ s = device.summary
79
+ info["firmware_version"] = s.version
80
+ if s.tags:
81
+ if s.tags.geoip:
82
+ g = s.tags.geoip
83
+ info["geoip"] = {
84
+ "city": g.city,
85
+ "state": g.state,
86
+ "country": g.country,
87
+ "country_code": g.country_code,
88
+ "isp_name": g.isp_name,
89
+ "latitude": g.latitude,
90
+ "longitude": g.longitude,
91
+ }
92
+ if s.tags.device_info:
93
+ di = s.tags.device_info
94
+ info["device_info"] = {
95
+ "name": di.name,
96
+ "full_name": di.full_name,
97
+ "version": di.version,
98
+ "cpu_count": di.cpu_count,
99
+ "operating_system": di.operating_system,
100
+ }
101
+ if s.tags.network_interface:
102
+ ni = s.tags.network_interface
103
+ info["network_interface"] = {
104
+ "name": ni.name,
105
+ "type": ni.type,
106
+ "local_ip": ni.local_ip,
107
+ "mac_address": ni.mac_address,
108
+ }
109
+
110
+ return {
111
+ "orb_id": device.orb_id,
112
+ "name": device.name,
113
+ "can_notify": device.can_notify,
114
+ "tags": device.tags,
115
+ "config": device.config,
116
+ "created_ts": device.created_ts,
117
+ **info,
118
+ }
119
+
120
+
121
+ def _serialize_telemetry(device: Device) -> dict[str, Any]:
122
+ """Serialize the live, frequently-changing fields of a device."""
123
+ scores: dict[str, Any] = {}
124
+ if device.summary:
125
+ s = device.summary
126
+ scores["summary_ts"] = s.created_ts
127
+ if s.orb_score:
128
+ scores["orb_score"] = _serialize_score(s.orb_score)
129
+ if s.orb_scores:
130
+ scores["orb_scores"] = [_serialize_score(sc) for sc in s.orb_scores]
131
+
132
+ return {
133
+ "orb_id": device.orb_id,
134
+ "name": device.name,
135
+ "is_connected": device.is_connected.value,
136
+ "is_connected_updated_at": device.is_connected_updated_at,
137
+ **scores,
138
+ }
@@ -0,0 +1,57 @@
1
+ """MCP tools and resources for Orb Cloud organizations."""
2
+
3
+ from typing import Any
4
+
5
+ from orb_cloud_client.models.server import OrganizationsResponse
6
+
7
+ from orb_cloud_mcp.cache import cache, cache_key
8
+ from orb_cloud_mcp.client import get_client
9
+
10
+ _CACHE_KEY = "list_organizations"
11
+
12
+
13
+ async def list_organizations() -> list[dict[str, Any]]:
14
+ """List all Orb Cloud organizations accessible with the configured API key."""
15
+ key = cache_key(_CACHE_KEY)
16
+ if key in cache:
17
+ return cache[key]
18
+
19
+ async with get_client() as client:
20
+ orgs = await client.get_organizations()
21
+ result = [_serialize_org(o) for o in orgs]
22
+ cache[key] = result
23
+ return result
24
+
25
+
26
+ def _serialize_org(org: OrganizationsResponse) -> dict[str, Any]:
27
+ plan = None
28
+ if org.plan:
29
+ limits = None
30
+ if org.plan.limits:
31
+ limits = {
32
+ "devices": org.plan.limits.devices,
33
+ "users": org.plan.limits.users,
34
+ "deployment_tokens": org.plan.limits.deployment_tokens,
35
+ }
36
+ plan = {
37
+ "name": org.plan.name,
38
+ "features": org.plan.features,
39
+ "limits": limits,
40
+ }
41
+
42
+ usage = None
43
+ if org.usage:
44
+ usage = {
45
+ "devices": org.usage.devices,
46
+ "users": org.usage.users,
47
+ "deployment_tokens": org.usage.deployment_tokens,
48
+ }
49
+
50
+ return {
51
+ "organization_id": org.organization_id,
52
+ "name": org.name,
53
+ "customer_id": org.customer_id,
54
+ "subscription_id": org.subscription_id,
55
+ "plan": plan,
56
+ "usage": usage,
57
+ }
@@ -0,0 +1,20 @@
1
+ """MCP tool to trigger speed tests on Orb Cloud devices."""
2
+
3
+ from typing import Any, Literal
4
+
5
+ from orb_cloud_mcp.client import get_client
6
+
7
+
8
+ async def trigger_speedtest(
9
+ device_id: str,
10
+ test_type: Literal["content", "top"],
11
+ ) -> dict[str, Any]:
12
+ """Trigger a speed test on an Orb Cloud device.
13
+
14
+ Args:
15
+ device_id: The Orb device ID to run the speed test on.
16
+ test_type: The type of speed test — 'content' measures content delivery
17
+ performance, 'top' measures peak throughput.
18
+ """
19
+ async with get_client() as client:
20
+ return await client.trigger_speedtest(device_id, test_type)
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: orb-cloud-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for Orb Cloud device management and data analysis
5
+ Author: Brian Connelly
6
+ Author-email: Brian Connelly <bdc@bconnelly.net>
7
+ License-Expression: MIT
8
+ Requires-Dist: cachetools>=7.0.4
9
+ Requires-Dist: fastmcp>=2.0
10
+ Requires-Dist: orb-cloud-client>=1.3.3
11
+ Requires-Dist: pydantic>=2.0
12
+ Requires-Python: >=3.10
@@ -0,0 +1,14 @@
1
+ orb_cloud_mcp/__init__.py,sha256=iwtL2XPlsOy5MtEiGxCo2RlWFykz_dN8mE9j92hblV4,73
2
+ orb_cloud_mcp/cache.py,sha256=pbEIlyf6gGVYPZ7UuiQOiuDfU4FyK8HcRHU8km0Pvxc,731
3
+ orb_cloud_mcp/client.py,sha256=m_gjsp_tTuxtay2C-rNMJ1u3_nqoblqZreoaiYO61MI,574
4
+ orb_cloud_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ orb_cloud_mcp/server.py,sha256=d_DU7v65X1iz4OYgEW0FzA4E-C_O__gV5aOL0mRshvs,1177
6
+ orb_cloud_mcp/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ orb_cloud_mcp/tools/datasets.py,sha256=x0cvwbmw-Zxdg4dT9dRUKiPNrnVwrR-v1nIStnDcAzo,4087
8
+ orb_cloud_mcp/tools/devices.py,sha256=P3QkPkjCDsyP8Vc8gzkDdL-RAjP_7ZUoXFjBbvMCBJU,4555
9
+ orb_cloud_mcp/tools/organizations.py,sha256=CFnSkSa9-uWzH30vieDmdypFRFMPkie3CqQ2pfkZZww,1604
10
+ orb_cloud_mcp/tools/speedtest.py,sha256=skTpedUdS07ZHaFJ_MoXrbyDZvFNH5532gZ2fHNzcpI,632
11
+ orb_cloud_mcp-0.1.0.dist-info/WHEEL,sha256=mydTeHxOpFHo-DnYhAd_3ATePms-g4rrYvM7wJK8P-U,80
12
+ orb_cloud_mcp-0.1.0.dist-info/entry_points.txt,sha256=px4lDagsuu_DHSLjW0IaQ0qvkXLp-7Yff6CvYMddamg,61
13
+ orb_cloud_mcp-0.1.0.dist-info/METADATA,sha256=WpWZmZ_kiSF8H5A8J2Zr1zvQRg7a5UE_fbBC1PQk2Vw,376
14
+ orb_cloud_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.10.9
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ orb-cloud-mcp = orb_cloud_mcp.server:main
3
+