Spaces:
Running
Running
Merge pull request #237 from jlowin/nits
Browse files
examples/smart_home/src/smart_home/lights/server.py
CHANGED
|
@@ -11,15 +11,28 @@ from smart_home.lights.hue_utils import _get_bridge, handle_phue_error
|
|
| 11 |
class HueAttributes(TypedDict, total=False):
|
| 12 |
"""TypedDict for optional light attributes."""
|
| 13 |
|
| 14 |
-
on: NotRequired[bool]
|
| 15 |
-
bri: NotRequired[Annotated[int, Field(ge=0, le=254)]]
|
| 16 |
-
hue: NotRequired[
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
alert: NotRequired[Literal["none", "select", "lselect"]]
|
| 21 |
effect: NotRequired[Literal["none", "colorloop"]]
|
| 22 |
-
transitiontime: NotRequired[int
|
| 23 |
|
| 24 |
|
| 25 |
lights_mcp = FastMCP(
|
|
@@ -161,7 +174,7 @@ def activate_scene(group_name: str, scene_name: str) -> dict[str, Any]:
|
|
| 161 |
scenes_data = bridge.get_scene()
|
| 162 |
scene_found = False
|
| 163 |
scene_in_correct_group = False
|
| 164 |
-
for
|
| 165 |
if sinfo.get("name") == scene_name:
|
| 166 |
scene_found = True
|
| 167 |
# Check if this scene is associated with the target group ID
|
|
@@ -217,7 +230,7 @@ def set_light_attributes(light_name: str, attributes: HueAttributes) -> dict[str
|
|
| 217 |
}
|
| 218 |
|
| 219 |
try:
|
| 220 |
-
result = bridge.set_light(light_name, attributes)
|
| 221 |
return {
|
| 222 |
"light": light_name,
|
| 223 |
"set_attributes": attributes,
|
|
@@ -243,7 +256,7 @@ def set_group_attributes(group_name: str, attributes: HueAttributes) -> dict[str
|
|
| 243 |
}
|
| 244 |
|
| 245 |
try:
|
| 246 |
-
result = bridge.set_group(group_name, attributes)
|
| 247 |
return {
|
| 248 |
"group": group_name,
|
| 249 |
"set_attributes": attributes,
|
|
@@ -269,7 +282,7 @@ def list_lights_by_group() -> dict[str, list[str]] | list[str]:
|
|
| 269 |
lights_data = bridge.get_light_objects("id") # dict {light_id: {details}}
|
| 270 |
|
| 271 |
lights_by_group: dict[str, list[str]] = {}
|
| 272 |
-
for
|
| 273 |
group_name = group_details.get("name")
|
| 274 |
light_ids = group_details.get("lights", [])
|
| 275 |
if group_name and light_ids:
|
|
@@ -282,11 +295,10 @@ def list_lights_by_group() -> dict[str, list[str]] | list[str]:
|
|
| 282 |
if light_name:
|
| 283 |
light_names.append(light_name)
|
| 284 |
if light_names:
|
| 285 |
-
light_names.sort()
|
| 286 |
lights_by_group[group_name] = light_names
|
| 287 |
|
| 288 |
return lights_by_group
|
| 289 |
|
| 290 |
except (PhueException, Exception) as e:
|
| 291 |
-
# Return error as list
|
| 292 |
return [f"Error listing lights by group: {e}"]
|
|
|
|
| 11 |
class HueAttributes(TypedDict, total=False):
|
| 12 |
"""TypedDict for optional light attributes."""
|
| 13 |
|
| 14 |
+
on: NotRequired[Annotated[bool, Field(description="on/off state")]]
|
| 15 |
+
bri: NotRequired[Annotated[int, Field(ge=0, le=254, description="brightness")]]
|
| 16 |
+
hue: NotRequired[
|
| 17 |
+
Annotated[
|
| 18 |
+
int,
|
| 19 |
+
Field(
|
| 20 |
+
ge=0,
|
| 21 |
+
le=254,
|
| 22 |
+
description="saturation",
|
| 23 |
+
),
|
| 24 |
+
]
|
| 25 |
+
]
|
| 26 |
+
xy: NotRequired[Annotated[list[float], Field(description="xy color coordinates")]]
|
| 27 |
+
ct: NotRequired[
|
| 28 |
+
Annotated[
|
| 29 |
+
int,
|
| 30 |
+
Field(ge=153, le=500, description="color temperature"),
|
| 31 |
+
]
|
| 32 |
+
]
|
| 33 |
alert: NotRequired[Literal["none", "select", "lselect"]]
|
| 34 |
effect: NotRequired[Literal["none", "colorloop"]]
|
| 35 |
+
transitiontime: NotRequired[Annotated[int, Field(description="deciseconds")]]
|
| 36 |
|
| 37 |
|
| 38 |
lights_mcp = FastMCP(
|
|
|
|
| 174 |
scenes_data = bridge.get_scene()
|
| 175 |
scene_found = False
|
| 176 |
scene_in_correct_group = False
|
| 177 |
+
for sinfo in scenes_data.values():
|
| 178 |
if sinfo.get("name") == scene_name:
|
| 179 |
scene_found = True
|
| 180 |
# Check if this scene is associated with the target group ID
|
|
|
|
| 230 |
}
|
| 231 |
|
| 232 |
try:
|
| 233 |
+
result = bridge.set_light(light_name, dict(attributes))
|
| 234 |
return {
|
| 235 |
"light": light_name,
|
| 236 |
"set_attributes": attributes,
|
|
|
|
| 256 |
}
|
| 257 |
|
| 258 |
try:
|
| 259 |
+
result = bridge.set_group(group_name, dict(attributes))
|
| 260 |
return {
|
| 261 |
"group": group_name,
|
| 262 |
"set_attributes": attributes,
|
|
|
|
| 282 |
lights_data = bridge.get_light_objects("id") # dict {light_id: {details}}
|
| 283 |
|
| 284 |
lights_by_group: dict[str, list[str]] = {}
|
| 285 |
+
for group_details in groups_data.values():
|
| 286 |
group_name = group_details.get("name")
|
| 287 |
light_ids = group_details.get("lights", [])
|
| 288 |
if group_name and light_ids:
|
|
|
|
| 295 |
if light_name:
|
| 296 |
light_names.append(light_name)
|
| 297 |
if light_names:
|
| 298 |
+
light_names.sort()
|
| 299 |
lights_by_group[group_name] = light_names
|
| 300 |
|
| 301 |
return lights_by_group
|
| 302 |
|
| 303 |
except (PhueException, Exception) as e:
|
|
|
|
| 304 |
return [f"Error listing lights by group: {e}"]
|
src/fastmcp/__init__.py
CHANGED
|
@@ -14,6 +14,7 @@ __all__ = [
|
|
| 14 |
"FastMCP",
|
| 15 |
"Context",
|
| 16 |
"client",
|
|
|
|
| 17 |
"settings",
|
| 18 |
"Image",
|
| 19 |
]
|
|
|
|
| 14 |
"FastMCP",
|
| 15 |
"Context",
|
| 16 |
"client",
|
| 17 |
+
"Client",
|
| 18 |
"settings",
|
| 19 |
"Image",
|
| 20 |
]
|
src/fastmcp/server/server.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
"""FastMCP - A more ergonomic interface for MCP servers."""
|
| 2 |
|
|
|
|
|
|
|
| 3 |
import datetime
|
| 4 |
from collections.abc import AsyncIterator, Awaitable, Callable
|
| 5 |
from contextlib import (
|
|
@@ -63,7 +65,7 @@ class MountedServer:
|
|
| 63 |
def __init__(
|
| 64 |
self,
|
| 65 |
prefix: str,
|
| 66 |
-
server:
|
| 67 |
tool_separator: str | None = None,
|
| 68 |
resource_separator: str | None = None,
|
| 69 |
prompt_separator: str | None = None,
|
|
@@ -149,7 +151,7 @@ class TimedCache:
|
|
| 149 |
|
| 150 |
|
| 151 |
@asynccontextmanager
|
| 152 |
-
async def default_lifespan(server:
|
| 153 |
"""Default lifespan context manager that does nothing.
|
| 154 |
|
| 155 |
Args:
|
|
@@ -162,8 +164,8 @@ async def default_lifespan(server: "FastMCP") -> AsyncIterator[Any]:
|
|
| 162 |
|
| 163 |
|
| 164 |
def _lifespan_wrapper(
|
| 165 |
-
app:
|
| 166 |
-
lifespan: Callable[[
|
| 167 |
) -> Callable[
|
| 168 |
[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
| 169 |
]:
|
|
@@ -182,7 +184,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 182 |
name: str | None = None,
|
| 183 |
instructions: str | None = None,
|
| 184 |
lifespan: (
|
| 185 |
-
Callable[
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
) = None,
|
| 187 |
tags: set[str] | None = None,
|
| 188 |
**settings: Any,
|
|
@@ -273,7 +279,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 273 |
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
| 274 |
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
| 275 |
|
| 276 |
-
def get_context(self) ->
|
| 277 |
"""
|
| 278 |
Returns a Context object. Note that the context will only be valid
|
| 279 |
during a request; outside a request, most methods will error.
|
|
@@ -766,7 +772,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 766 |
def mount(
|
| 767 |
self,
|
| 768 |
prefix: str,
|
| 769 |
-
server:
|
| 770 |
tool_separator: str | None = None,
|
| 771 |
resource_separator: str | None = None,
|
| 772 |
prompt_separator: str | None = None,
|
|
@@ -791,7 +797,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 791 |
async def import_server(
|
| 792 |
self,
|
| 793 |
prefix: str,
|
| 794 |
-
server:
|
| 795 |
tool_separator: str | None = None,
|
| 796 |
resource_separator: str | None = None,
|
| 797 |
prompt_separator: str | None = None,
|
|
@@ -865,7 +871,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 865 |
@classmethod
|
| 866 |
def from_openapi(
|
| 867 |
cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
|
| 868 |
-
) ->
|
| 869 |
"""
|
| 870 |
Create a FastMCP server from an OpenAPI specification.
|
| 871 |
"""
|
|
@@ -875,8 +881,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 875 |
|
| 876 |
@classmethod
|
| 877 |
def from_fastapi(
|
| 878 |
-
cls, app:
|
| 879 |
-
) ->
|
| 880 |
"""
|
| 881 |
Create a FastMCP server from a FastAPI application.
|
| 882 |
"""
|
|
@@ -894,7 +900,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 894 |
)
|
| 895 |
|
| 896 |
@classmethod
|
| 897 |
-
def from_client(cls, client:
|
| 898 |
"""
|
| 899 |
Create a FastMCP proxy server from a FastMCP client.
|
| 900 |
"""
|
|
|
|
| 1 |
"""FastMCP - A more ergonomic interface for MCP servers."""
|
| 2 |
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
import datetime
|
| 6 |
from collections.abc import AsyncIterator, Awaitable, Callable
|
| 7 |
from contextlib import (
|
|
|
|
| 65 |
def __init__(
|
| 66 |
self,
|
| 67 |
prefix: str,
|
| 68 |
+
server: FastMCP,
|
| 69 |
tool_separator: str | None = None,
|
| 70 |
resource_separator: str | None = None,
|
| 71 |
prompt_separator: str | None = None,
|
|
|
|
| 151 |
|
| 152 |
|
| 153 |
@asynccontextmanager
|
| 154 |
+
async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
|
| 155 |
"""Default lifespan context manager that does nothing.
|
| 156 |
|
| 157 |
Args:
|
|
|
|
| 164 |
|
| 165 |
|
| 166 |
def _lifespan_wrapper(
|
| 167 |
+
app: FastMCP,
|
| 168 |
+
lifespan: Callable[[FastMCP], AbstractAsyncContextManager[LifespanResultT]],
|
| 169 |
) -> Callable[
|
| 170 |
[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
| 171 |
]:
|
|
|
|
| 184 |
name: str | None = None,
|
| 185 |
instructions: str | None = None,
|
| 186 |
lifespan: (
|
| 187 |
+
Callable[
|
| 188 |
+
[FastMCP[LifespanResultT]],
|
| 189 |
+
AbstractAsyncContextManager[LifespanResultT],
|
| 190 |
+
]
|
| 191 |
+
| None
|
| 192 |
) = None,
|
| 193 |
tags: set[str] | None = None,
|
| 194 |
**settings: Any,
|
|
|
|
| 279 |
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
| 280 |
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
| 281 |
|
| 282 |
+
def get_context(self) -> Context[ServerSession, LifespanResultT]:
|
| 283 |
"""
|
| 284 |
Returns a Context object. Note that the context will only be valid
|
| 285 |
during a request; outside a request, most methods will error.
|
|
|
|
| 772 |
def mount(
|
| 773 |
self,
|
| 774 |
prefix: str,
|
| 775 |
+
server: FastMCP[LifespanResultT],
|
| 776 |
tool_separator: str | None = None,
|
| 777 |
resource_separator: str | None = None,
|
| 778 |
prompt_separator: str | None = None,
|
|
|
|
| 797 |
async def import_server(
|
| 798 |
self,
|
| 799 |
prefix: str,
|
| 800 |
+
server: FastMCP[LifespanResultT],
|
| 801 |
tool_separator: str | None = None,
|
| 802 |
resource_separator: str | None = None,
|
| 803 |
prompt_separator: str | None = None,
|
|
|
|
| 871 |
@classmethod
|
| 872 |
def from_openapi(
|
| 873 |
cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
|
| 874 |
+
) -> FastMCPOpenAPI:
|
| 875 |
"""
|
| 876 |
Create a FastMCP server from an OpenAPI specification.
|
| 877 |
"""
|
|
|
|
| 881 |
|
| 882 |
@classmethod
|
| 883 |
def from_fastapi(
|
| 884 |
+
cls, app: Any, name: str | None = None, **settings: Any
|
| 885 |
+
) -> FastMCPOpenAPI:
|
| 886 |
"""
|
| 887 |
Create a FastMCP server from a FastAPI application.
|
| 888 |
"""
|
|
|
|
| 900 |
)
|
| 901 |
|
| 902 |
@classmethod
|
| 903 |
+
def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy:
|
| 904 |
"""
|
| 905 |
Create a FastMCP proxy server from a FastMCP client.
|
| 906 |
"""
|