fastmcp 0.2.0__py3-none-any.whl → 0.3.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,80 @@
1
+ """Resource template functionality."""
2
+
3
+ import inspect
4
+ import re
5
+ from typing import Any, Callable, Dict, Optional
6
+
7
+ from pydantic import BaseModel, Field, TypeAdapter, validate_call
8
+
9
+ from fastmcp.resources.types import FunctionResource, Resource
10
+
11
+
12
+ class ResourceTemplate(BaseModel):
13
+ """A template for dynamically creating resources."""
14
+
15
+ uri_template: str = Field(
16
+ description="URI template with parameters (e.g. weather://{city}/current)"
17
+ )
18
+ name: str = Field(description="Name of the resource")
19
+ description: str | None = Field(description="Description of what the resource does")
20
+ mime_type: str = Field(
21
+ default="text/plain", description="MIME type of the resource content"
22
+ )
23
+ fn: Callable = Field(exclude=True)
24
+ parameters: dict = Field(description="JSON schema for function parameters")
25
+
26
+ @classmethod
27
+ def from_function(
28
+ cls,
29
+ fn: Callable,
30
+ uri_template: str,
31
+ name: Optional[str] = None,
32
+ description: Optional[str] = None,
33
+ mime_type: Optional[str] = None,
34
+ ) -> "ResourceTemplate":
35
+ """Create a template from a function."""
36
+ func_name = name or fn.__name__
37
+ if func_name == "<lambda>":
38
+ raise ValueError("You must provide a name for lambda functions")
39
+
40
+ # Get schema from TypeAdapter - will fail if function isn't properly typed
41
+ parameters = TypeAdapter(fn).json_schema()
42
+
43
+ # ensure the arguments are properly cast
44
+ fn = validate_call(fn)
45
+
46
+ return cls(
47
+ uri_template=uri_template,
48
+ name=func_name,
49
+ description=description or fn.__doc__ or "",
50
+ mime_type=mime_type or "text/plain",
51
+ fn=fn,
52
+ parameters=parameters,
53
+ )
54
+
55
+ def matches(self, uri: str) -> Optional[Dict[str, Any]]:
56
+ """Check if URI matches template and extract parameters."""
57
+ # Convert template to regex pattern
58
+ pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
59
+ match = re.match(f"^{pattern}$", uri)
60
+ if match:
61
+ return match.groupdict()
62
+ return None
63
+
64
+ async def create_resource(self, uri: str, params: Dict[str, Any]) -> Resource:
65
+ """Create a resource from the template with the given parameters."""
66
+ try:
67
+ # Call function and check if result is a coroutine
68
+ result = self.fn(**params)
69
+ if inspect.iscoroutine(result):
70
+ result = await result
71
+
72
+ return FunctionResource(
73
+ uri=uri,
74
+ name=self.name,
75
+ description=self.description,
76
+ mime_type=self.mime_type,
77
+ fn=lambda: result, # Capture result in closure
78
+ )
79
+ except Exception as e:
80
+ raise ValueError(f"Error creating resource from template: {e}")
@@ -0,0 +1,171 @@
1
+ """Concrete resource implementations."""
2
+
3
+ import asyncio
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Callable, Union
7
+
8
+ import httpx
9
+ import pydantic.json
10
+ import pydantic_core
11
+ from pydantic import Field
12
+
13
+ from fastmcp.resources.base import Resource
14
+
15
+
16
+ class TextResource(Resource):
17
+ """A resource that reads from a string."""
18
+
19
+ text: str = Field(description="Text content of the resource")
20
+
21
+ async def read(self) -> str:
22
+ """Read the text content."""
23
+ return self.text
24
+
25
+
26
+ class BinaryResource(Resource):
27
+ """A resource that reads from bytes."""
28
+
29
+ data: bytes = Field(description="Binary content of the resource")
30
+
31
+ async def read(self) -> bytes:
32
+ """Read the binary content."""
33
+ return self.data
34
+
35
+
36
+ class FunctionResource(Resource):
37
+ """A resource that defers data loading by wrapping a function.
38
+
39
+ The function is only called when the resource is read, allowing for lazy loading
40
+ of potentially expensive data. This is particularly useful when listing resources,
41
+ as the function won't be called until the resource is actually accessed.
42
+
43
+ The function can return:
44
+ - str for text content (default)
45
+ - bytes for binary content
46
+ - other types will be converted to JSON
47
+ """
48
+
49
+ fn: Callable[[], Any] = Field(exclude=True)
50
+
51
+ async def read(self) -> Union[str, bytes]:
52
+ """Read the resource by calling the wrapped function."""
53
+ try:
54
+ result = self.fn()
55
+ if isinstance(result, Resource):
56
+ return await result.read()
57
+ if isinstance(result, bytes):
58
+ return result
59
+ if isinstance(result, str):
60
+ return result
61
+ try:
62
+ return json.dumps(pydantic_core.to_jsonable_python(result))
63
+ except (TypeError, pydantic_core.PydanticSerializationError):
64
+ # If JSON serialization fails, try str()
65
+ return str(result)
66
+ except Exception as e:
67
+ raise ValueError(f"Error reading resource {self.uri}: {e}")
68
+
69
+
70
+ class FileResource(Resource):
71
+ """A resource that reads from a file.
72
+
73
+ Set is_binary=True to read file as binary data instead of text.
74
+ """
75
+
76
+ path: Path = Field(description="Path to the file")
77
+ is_binary: bool = Field(
78
+ default=False,
79
+ description="Whether to read the file as binary data",
80
+ )
81
+ mime_type: str = Field(
82
+ default="text/plain",
83
+ description="MIME type of the resource content",
84
+ )
85
+
86
+ @pydantic.field_validator("path")
87
+ @classmethod
88
+ def validate_absolute_path(cls, path: Path) -> Path:
89
+ """Ensure path is absolute."""
90
+ if not path.is_absolute():
91
+ raise ValueError("Path must be absolute")
92
+ return path
93
+
94
+ async def read(self) -> Union[str, bytes]:
95
+ """Read the file content."""
96
+ try:
97
+ if self.is_binary:
98
+ return await asyncio.to_thread(self.path.read_bytes)
99
+ return await asyncio.to_thread(self.path.read_text)
100
+ except Exception as e:
101
+ raise ValueError(f"Error reading file {self.path}: {e}")
102
+
103
+
104
+ class HttpResource(Resource):
105
+ """A resource that reads from an HTTP endpoint."""
106
+
107
+ url: str = Field(description="URL to fetch content from")
108
+ mime_type: str | None = Field(
109
+ default="application/json", description="MIME type of the resource content"
110
+ )
111
+
112
+ async def read(self) -> Union[str, bytes]:
113
+ """Read the HTTP content."""
114
+ async with httpx.AsyncClient() as client:
115
+ response = await client.get(self.url)
116
+ response.raise_for_status()
117
+ return response.text
118
+
119
+
120
+ class DirectoryResource(Resource):
121
+ """A resource that lists files in a directory."""
122
+
123
+ path: Path = Field(description="Path to the directory")
124
+ recursive: bool = Field(
125
+ default=False, description="Whether to list files recursively"
126
+ )
127
+ pattern: str | None = Field(
128
+ default=None, description="Optional glob pattern to filter files"
129
+ )
130
+ mime_type: str | None = Field(
131
+ default="application/json", description="MIME type of the resource content"
132
+ )
133
+
134
+ @pydantic.field_validator("path")
135
+ @classmethod
136
+ def validate_absolute_path(cls, path: Path) -> Path:
137
+ """Ensure path is absolute."""
138
+ if not path.is_absolute():
139
+ raise ValueError("Path must be absolute")
140
+ return path
141
+
142
+ def list_files(self) -> list[Path]:
143
+ """List files in the directory."""
144
+ if not self.path.exists():
145
+ raise FileNotFoundError(f"Directory not found: {self.path}")
146
+ if not self.path.is_dir():
147
+ raise NotADirectoryError(f"Not a directory: {self.path}")
148
+
149
+ try:
150
+ if self.pattern:
151
+ return (
152
+ list(self.path.glob(self.pattern))
153
+ if not self.recursive
154
+ else list(self.path.rglob(self.pattern))
155
+ )
156
+ return (
157
+ list(self.path.glob("*"))
158
+ if not self.recursive
159
+ else list(self.path.rglob("*"))
160
+ )
161
+ except Exception as e:
162
+ raise ValueError(f"Error listing directory {self.path}: {e}")
163
+
164
+ async def read(self) -> str: # Always returns JSON string
165
+ """Read the directory listing."""
166
+ try:
167
+ files = await asyncio.to_thread(self.list_files)
168
+ file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
169
+ return json.dumps({"files": file_list}, indent=2)
170
+ except Exception as e:
171
+ raise ValueError(f"Error reading directory {self.path}: {e}")