fastmcp 1.0__py3-none-any.whl → 2.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.
Files changed (39) hide show
  1. fastmcp/__init__.py +15 -4
  2. fastmcp/cli/__init__.py +0 -1
  3. fastmcp/cli/claude.py +13 -11
  4. fastmcp/cli/cli.py +59 -39
  5. fastmcp/client/__init__.py +25 -0
  6. fastmcp/client/base.py +1 -0
  7. fastmcp/client/client.py +226 -0
  8. fastmcp/client/roots.py +75 -0
  9. fastmcp/client/sampling.py +50 -0
  10. fastmcp/client/transports.py +411 -0
  11. fastmcp/prompts/__init__.py +2 -2
  12. fastmcp/prompts/{base.py → prompt.py} +47 -26
  13. fastmcp/prompts/prompt_manager.py +69 -15
  14. fastmcp/resources/__init__.py +6 -6
  15. fastmcp/resources/{base.py → resource.py} +21 -2
  16. fastmcp/resources/resource_manager.py +116 -17
  17. fastmcp/resources/{templates.py → template.py} +36 -11
  18. fastmcp/resources/types.py +18 -13
  19. fastmcp/server/__init__.py +5 -0
  20. fastmcp/server/context.py +222 -0
  21. fastmcp/server/openapi.py +637 -0
  22. fastmcp/server/proxy.py +223 -0
  23. fastmcp/{server.py → server/server.py} +323 -267
  24. fastmcp/settings.py +81 -0
  25. fastmcp/tools/__init__.py +1 -1
  26. fastmcp/tools/{base.py → tool.py} +47 -18
  27. fastmcp/tools/tool_manager.py +57 -16
  28. fastmcp/utilities/func_metadata.py +33 -19
  29. fastmcp/utilities/openapi.py +797 -0
  30. fastmcp/utilities/types.py +15 -4
  31. fastmcp-2.1.0.dist-info/METADATA +770 -0
  32. fastmcp-2.1.0.dist-info/RECORD +39 -0
  33. fastmcp-2.1.0.dist-info/licenses/LICENSE +201 -0
  34. fastmcp/prompts/manager.py +0 -50
  35. fastmcp-1.0.dist-info/METADATA +0 -604
  36. fastmcp-1.0.dist-info/RECORD +0 -28
  37. fastmcp-1.0.dist-info/licenses/LICENSE +0 -21
  38. {fastmcp-1.0.dist-info → fastmcp-2.1.0.dist-info}/WHEEL +0 -0
  39. {fastmcp-1.0.dist-info → fastmcp-2.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,9 +1,10 @@
1
1
  """Prompt management functionality."""
2
2
 
3
- from typing import Dict, Optional
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import Any
4
5
 
5
-
6
- from fastmcp.prompts.base import Prompt
6
+ from fastmcp.prompts.prompt import Message, Prompt, PromptResult
7
+ from fastmcp.settings import DuplicateBehavior
7
8
  from fastmcp.utilities.logging import get_logger
8
9
 
9
10
  logger = get_logger(__name__)
@@ -12,25 +13,78 @@ logger = get_logger(__name__)
12
13
  class PromptManager:
13
14
  """Manages FastMCP prompts."""
14
15
 
15
- def __init__(self, warn_on_duplicate_prompts: bool = True):
16
- self._prompts: Dict[str, Prompt] = {}
17
- self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
16
+ def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
17
+ self._prompts: dict[str, Prompt] = {}
18
+ self.duplicate_behavior = duplicate_behavior
19
+
20
+ def get_prompt(self, name: str) -> Prompt | None:
21
+ """Get prompt by name."""
22
+ return self._prompts.get(name)
23
+
24
+ def list_prompts(self) -> list[Prompt]:
25
+ """List all registered prompts."""
26
+ return list(self._prompts.values())
27
+
28
+ def add_prompt_from_fn(
29
+ self,
30
+ fn: Callable[..., PromptResult | Awaitable[PromptResult]],
31
+ name: str | None = None,
32
+ description: str | None = None,
33
+ tags: set[str] | None = None,
34
+ ) -> Prompt:
35
+ """Create a prompt from a function."""
36
+ prompt = Prompt.from_function(fn, name=name, description=description, tags=tags)
37
+ return self.add_prompt(prompt)
18
38
 
19
39
  def add_prompt(self, prompt: Prompt) -> Prompt:
20
40
  """Add a prompt to the manager."""
21
- logger.debug(f"Adding prompt: {prompt.name}")
41
+
42
+ # Check for duplicates
22
43
  existing = self._prompts.get(prompt.name)
23
44
  if existing:
24
- if self.warn_on_duplicate_prompts:
45
+ if self.duplicate_behavior == DuplicateBehavior.WARN:
25
46
  logger.warning(f"Prompt already exists: {prompt.name}")
26
- return existing
47
+ self._prompts[prompt.name] = prompt
48
+ elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
49
+ self._prompts[prompt.name] = prompt
50
+ elif self.duplicate_behavior == DuplicateBehavior.ERROR:
51
+ raise ValueError(f"Prompt already exists: {prompt.name}")
52
+ elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
53
+ pass
54
+
27
55
  self._prompts[prompt.name] = prompt
28
56
  return prompt
29
57
 
30
- def get_prompt(self, name: str) -> Optional[Prompt]:
31
- """Get prompt by name."""
32
- return self._prompts.get(name)
58
+ async def render_prompt(
59
+ self, name: str, arguments: dict[str, Any] | None = None
60
+ ) -> list[Message]:
61
+ """Render a prompt by name with arguments."""
62
+ prompt = self.get_prompt(name)
63
+ if not prompt:
64
+ raise ValueError(f"Unknown prompt: {name}")
33
65
 
34
- def list_prompts(self) -> list[Prompt]:
35
- """List all registered prompts."""
36
- return list(self._prompts.values())
66
+ return await prompt.render(arguments)
67
+
68
+ def import_prompts(
69
+ self, manager: "PromptManager", prefix: str | None = None
70
+ ) -> None:
71
+ """
72
+ Import all prompts from another PromptManager with prefixed names.
73
+
74
+ Args:
75
+ manager: Another PromptManager instance to import prompts from
76
+ prefix: Prefix to add to prompt names. The resulting prompt name will
77
+ be in the format "{prefix}{original_name}" if prefix is provided,
78
+ otherwise the original name is used.
79
+ For example, with prefix "weather/" and prompt "forecast_prompt",
80
+ the imported prompt would be available as "weather/forecast_prompt"
81
+ """
82
+ for name, prompt in manager._prompts.items():
83
+ # Create prefixed name
84
+ prefixed_name = f"{prefix}{name}" if prefix else name
85
+
86
+ new_prompt = prompt.copy(updates=dict(name=prefixed_name))
87
+
88
+ # Store the prompt with the prefixed name
89
+ self.add_prompt(new_prompt)
90
+ logger.debug(f'Imported prompt "{name}" as "{prefixed_name}"')
@@ -1,14 +1,14 @@
1
- from .base import Resource
1
+ from .resource import Resource
2
+ from .resource_manager import ResourceManager
3
+ from .template import ResourceTemplate
2
4
  from .types import (
3
- TextResource,
4
5
  BinaryResource,
5
- FunctionResource,
6
+ DirectoryResource,
6
7
  FileResource,
8
+ FunctionResource,
7
9
  HttpResource,
8
- DirectoryResource,
10
+ TextResource,
9
11
  )
10
- from .templates import ResourceTemplate
11
- from .resource_manager import ResourceManager
12
12
 
13
13
  __all__ = [
14
14
  "Resource",
@@ -1,17 +1,21 @@
1
1
  """Base classes and interfaces for FastMCP resources."""
2
2
 
3
3
  import abc
4
- from typing import Union, Annotated
4
+ from typing import Annotated, Any
5
5
 
6
6
  from pydantic import (
7
7
  AnyUrl,
8
8
  BaseModel,
9
+ BeforeValidator,
9
10
  ConfigDict,
10
11
  Field,
11
12
  UrlConstraints,
12
13
  ValidationInfo,
13
14
  field_validator,
14
15
  )
16
+ from typing_extensions import Self
17
+
18
+ from fastmcp.utilities.types import _convert_set_defaults
15
19
 
16
20
 
17
21
  class Resource(BaseModel, abc.ABC):
@@ -26,6 +30,9 @@ class Resource(BaseModel, abc.ABC):
26
30
  description: str | None = Field(
27
31
  description="Description of the resource", default=None
28
32
  )
33
+ tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
34
+ default_factory=set, description="Tags for the resource"
35
+ )
29
36
  mime_type: str = Field(
30
37
  default="text/plain",
31
38
  description="MIME type of the resource content",
@@ -43,6 +50,18 @@ class Resource(BaseModel, abc.ABC):
43
50
  raise ValueError("Either name or uri must be provided")
44
51
 
45
52
  @abc.abstractmethod
46
- async def read(self) -> Union[str, bytes]:
53
+ async def read(self) -> str | bytes:
47
54
  """Read the resource content."""
48
55
  pass
56
+
57
+ def copy(self, updates: dict[str, Any] | None = None) -> Self:
58
+ """Copy the resource with optional updates."""
59
+ data = self.model_dump()
60
+ if updates:
61
+ data.update(updates)
62
+ return type(self)(**data)
63
+
64
+ def __eq__(self, other: object) -> bool:
65
+ if not isinstance(other, Resource):
66
+ return False
67
+ return self.model_dump() == other.model_dump()
@@ -1,11 +1,13 @@
1
1
  """Resource manager functionality."""
2
2
 
3
- from typing import Callable, Dict, Optional, Union
3
+ from collections.abc import Callable
4
+ from typing import Any
4
5
 
5
6
  from pydantic import AnyUrl
6
7
 
7
- from fastmcp.resources.base import Resource
8
- from fastmcp.resources.templates import ResourceTemplate
8
+ from fastmcp.resources.resource import Resource
9
+ from fastmcp.resources.template import ResourceTemplate
10
+ from fastmcp.settings import DuplicateBehavior
9
11
  from fastmcp.utilities.logging import get_logger
10
12
 
11
13
  logger = get_logger(__name__)
@@ -14,10 +16,10 @@ logger = get_logger(__name__)
14
16
  class ResourceManager:
15
17
  """Manages FastMCP resources."""
16
18
 
17
- def __init__(self, warn_on_duplicate_resources: bool = True):
18
- self._resources: Dict[str, Resource] = {}
19
- self._templates: Dict[str, ResourceTemplate] = {}
20
- self.warn_on_duplicate_resources = warn_on_duplicate_resources
19
+ def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
20
+ self._resources: dict[str, Resource] = {}
21
+ self._templates: dict[str, ResourceTemplate] = {}
22
+ self.duplicate_behavior = duplicate_behavior
21
23
 
22
24
  def add_resource(self, resource: Resource) -> Resource:
23
25
  """Add a resource to the manager.
@@ -34,37 +36,76 @@ class ResourceManager:
34
36
  extra={
35
37
  "uri": resource.uri,
36
38
  "type": type(resource).__name__,
37
- "name": resource.name,
39
+ "resource_name": resource.name,
38
40
  },
39
41
  )
40
42
  existing = self._resources.get(str(resource.uri))
41
43
  if existing:
42
- if self.warn_on_duplicate_resources:
44
+ if self.duplicate_behavior == DuplicateBehavior.WARN:
43
45
  logger.warning(f"Resource already exists: {resource.uri}")
44
- return existing
46
+ self._resources[str(resource.uri)] = resource
47
+ elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
48
+ self._resources[str(resource.uri)] = resource
49
+ elif self.duplicate_behavior == DuplicateBehavior.ERROR:
50
+ raise ValueError(f"Resource already exists: {resource.uri}")
51
+ elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
52
+ pass
45
53
  self._resources[str(resource.uri)] = resource
46
54
  return resource
47
55
 
48
- def add_template(
56
+ def add_template_from_fn(
49
57
  self,
50
- fn: Callable,
58
+ fn: Callable[..., Any],
51
59
  uri_template: str,
52
- name: Optional[str] = None,
53
- description: Optional[str] = None,
54
- mime_type: Optional[str] = None,
60
+ name: str | None = None,
61
+ description: str | None = None,
62
+ mime_type: str | None = None,
63
+ tags: set[str] | None = None,
55
64
  ) -> ResourceTemplate:
56
- """Add a template from a function."""
65
+ """Create a template from a function."""
57
66
  template = ResourceTemplate.from_function(
58
67
  fn,
59
68
  uri_template=uri_template,
60
69
  name=name,
61
70
  description=description,
62
71
  mime_type=mime_type,
72
+ tags=tags,
63
73
  )
74
+ return self.add_template(template)
75
+
76
+ def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
77
+ """Add a template to the manager.
78
+
79
+ Args:
80
+ template: A ResourceTemplate instance to add
81
+
82
+ Returns:
83
+ The added template. If a template with the same URI already exists,
84
+ returns the existing template.
85
+ """
86
+ logger.debug(
87
+ "Adding resource",
88
+ extra={
89
+ "uri": template.uri_template,
90
+ "type": type(template).__name__,
91
+ "resource_name": template.name,
92
+ },
93
+ )
94
+ existing = self._templates.get(str(template.uri_template))
95
+ if existing:
96
+ if self.duplicate_behavior == DuplicateBehavior.WARN:
97
+ logger.warning(f"Resource already exists: {template.uri_template}")
98
+ self._templates[str(template.uri_template)] = template
99
+ elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
100
+ self._templates[str(template.uri_template)] = template
101
+ elif self.duplicate_behavior == DuplicateBehavior.ERROR:
102
+ raise ValueError(f"Resource already exists: {template.uri_template}")
103
+ elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
104
+ pass
64
105
  self._templates[template.uri_template] = template
65
106
  return template
66
107
 
67
- async def get_resource(self, uri: Union[AnyUrl, str]) -> Optional[Resource]:
108
+ async def get_resource(self, uri: AnyUrl | str) -> Resource | None:
68
109
  """Get resource by URI, checking concrete resources first, then templates."""
69
110
  uri_str = str(uri)
70
111
  logger.debug("Getting resource", extra={"uri": uri_str})
@@ -92,3 +133,61 @@ class ResourceManager:
92
133
  """List all registered templates."""
93
134
  logger.debug("Listing templates", extra={"count": len(self._templates)})
94
135
  return list(self._templates.values())
136
+
137
+ def import_resources(
138
+ self, manager: "ResourceManager", prefix: str | None = None
139
+ ) -> None:
140
+ """Import resources from another resource manager.
141
+
142
+ Resources are imported with a prefixed URI if a prefix is provided. For example,
143
+ if a resource has URI "data://users" and you import it with prefix "app+", the
144
+ imported resource will have URI "app+data://users". If no prefix is provided,
145
+ the original URI is used.
146
+
147
+ Args:
148
+ manager: The ResourceManager to import from
149
+ prefix: A prefix to apply to the resource URIs, including the delimiter.
150
+ For example, "app+" would result in URIs like "app+data://users".
151
+ If None, the original URI is used.
152
+ """
153
+ for uri, resource in manager._resources.items():
154
+ # Create prefixed URI and copy the resource with the new URI
155
+ prefixed_uri = f"{prefix}{uri}" if prefix else uri
156
+
157
+ new_resource = resource.copy(updates=dict(uri=prefixed_uri))
158
+
159
+ # Store directly in resources dictionary
160
+ self.add_resource(new_resource)
161
+ logger.debug(f'Imported resource "{uri}" as "{prefixed_uri}"')
162
+
163
+ def import_templates(
164
+ self, manager: "ResourceManager", prefix: str | None = None
165
+ ) -> None:
166
+ """Import resource templates from another resource manager.
167
+
168
+ Templates are imported with a prefixed URI template if a prefix is provided.
169
+ For example, if a template has URI template "data://users/{id}" and you import
170
+ it with prefix "app+", the imported template will have URI template
171
+ "app+data://users/{id}". If no prefix is provided, the original URI template is used.
172
+
173
+ Args:
174
+ manager: The ResourceManager to import templates from
175
+ prefix: A prefix to apply to the template URIs, including the delimiter.
176
+ For example, "app+" would result in URI templates like "app+data://users/{id}".
177
+ If None, the original URI template is used.
178
+ """
179
+ for uri_template, template in manager._templates.items():
180
+ # Create prefixed URI template and copy the template with the new URI template
181
+ prefixed_uri_template = (
182
+ f"{prefix}{uri_template}" if prefix else uri_template
183
+ )
184
+
185
+ new_template = template.copy(
186
+ updates=dict(uri_template=prefixed_uri_template)
187
+ )
188
+
189
+ # Store directly in templates dictionary
190
+ self.add_template(new_template)
191
+ logger.debug(
192
+ f'Imported template "{uri_template}" as "{prefixed_uri_template}"'
193
+ )
@@ -1,12 +1,17 @@
1
1
  """Resource template functionality."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import inspect
4
6
  import re
5
- from typing import Any, Callable, Dict, Optional
7
+ from collections.abc import Callable
8
+ from typing import Annotated, Any
6
9
 
7
- from pydantic import BaseModel, Field, TypeAdapter, validate_call
10
+ from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
11
+ from typing_extensions import Self
8
12
 
9
13
  from fastmcp.resources.types import FunctionResource, Resource
14
+ from fastmcp.utilities.types import _convert_set_defaults
10
15
 
11
16
 
12
17
  class ResourceTemplate(BaseModel):
@@ -17,21 +22,27 @@ class ResourceTemplate(BaseModel):
17
22
  )
18
23
  name: str = Field(description="Name of the resource")
19
24
  description: str | None = Field(description="Description of what the resource does")
25
+ tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
26
+ default_factory=set, description="Tags for the resource"
27
+ )
20
28
  mime_type: str = Field(
21
29
  default="text/plain", description="MIME type of the resource content"
22
30
  )
23
- fn: Callable = Field(exclude=True)
24
- parameters: dict = Field(description="JSON schema for function parameters")
31
+ fn: Callable[..., Any]
32
+ parameters: dict[str, Any] = Field(
33
+ description="JSON schema for function parameters"
34
+ )
25
35
 
26
36
  @classmethod
27
37
  def from_function(
28
38
  cls,
29
- fn: Callable,
39
+ fn: Callable[..., Any],
30
40
  uri_template: str,
31
- name: Optional[str] = None,
32
- description: Optional[str] = None,
33
- mime_type: Optional[str] = None,
34
- ) -> "ResourceTemplate":
41
+ name: str | None = None,
42
+ description: str | None = None,
43
+ mime_type: str | None = None,
44
+ tags: set[str] | None = None,
45
+ ) -> ResourceTemplate:
35
46
  """Create a template from a function."""
36
47
  func_name = name or fn.__name__
37
48
  if func_name == "<lambda>":
@@ -50,9 +61,10 @@ class ResourceTemplate(BaseModel):
50
61
  mime_type=mime_type or "text/plain",
51
62
  fn=fn,
52
63
  parameters=parameters,
64
+ tags=tags or set(),
53
65
  )
54
66
 
55
- def matches(self, uri: str) -> Optional[Dict[str, Any]]:
67
+ def matches(self, uri: str) -> dict[str, Any] | None:
56
68
  """Check if URI matches template and extract parameters."""
57
69
  # Convert template to regex pattern
58
70
  pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
@@ -61,7 +73,7 @@ class ResourceTemplate(BaseModel):
61
73
  return match.groupdict()
62
74
  return None
63
75
 
64
- async def create_resource(self, uri: str, params: Dict[str, Any]) -> Resource:
76
+ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
65
77
  """Create a resource from the template with the given parameters."""
66
78
  try:
67
79
  # Call function and check if result is a coroutine
@@ -75,6 +87,19 @@ class ResourceTemplate(BaseModel):
75
87
  description=self.description,
76
88
  mime_type=self.mime_type,
77
89
  fn=lambda: result, # Capture result in closure
90
+ tags=self.tags,
78
91
  )
79
92
  except Exception as e:
80
93
  raise ValueError(f"Error creating resource from template: {e}")
94
+
95
+ def copy(self, updates: dict[str, Any] | None = None) -> Self:
96
+ """Copy the resource template with optional updates."""
97
+ data = self.model_dump()
98
+ if updates:
99
+ data.update(updates)
100
+ return type(self)(**data)
101
+
102
+ def __eq__(self, other: object) -> bool:
103
+ if not isinstance(other, ResourceTemplate):
104
+ return False
105
+ return self.model_dump() == other.model_dump()
@@ -1,16 +1,19 @@
1
1
  """Concrete resource implementations."""
2
2
 
3
- import asyncio
3
+ import inspect
4
4
  import json
5
+ from collections.abc import Callable
5
6
  from pathlib import Path
6
- from typing import Any, Callable, Union
7
+ from typing import Any
7
8
 
9
+ import anyio
10
+ import anyio.to_thread
8
11
  import httpx
9
12
  import pydantic.json
10
13
  import pydantic_core
11
14
  from pydantic import Field, ValidationInfo
12
15
 
13
- from fastmcp.resources.base import Resource
16
+ from fastmcp.resources.resource import Resource
14
17
 
15
18
 
16
19
  class TextResource(Resource):
@@ -46,12 +49,14 @@ class FunctionResource(Resource):
46
49
  - other types will be converted to JSON
47
50
  """
48
51
 
49
- fn: Callable[[], Any] = Field(exclude=True)
52
+ fn: Callable[[], Any]
50
53
 
51
- async def read(self) -> Union[str, bytes]:
54
+ async def read(self) -> str | bytes:
52
55
  """Read the resource by calling the wrapped function."""
53
56
  try:
54
- result = self.fn()
57
+ result = (
58
+ await self.fn() if inspect.iscoroutinefunction(self.fn) else self.fn()
59
+ )
55
60
  if isinstance(result, Resource):
56
61
  return await result.read()
57
62
  if isinstance(result, bytes):
@@ -100,12 +105,12 @@ class FileResource(Resource):
100
105
  mime_type = info.data.get("mime_type", "text/plain")
101
106
  return not mime_type.startswith("text/")
102
107
 
103
- async def read(self) -> Union[str, bytes]:
108
+ async def read(self) -> str | bytes:
104
109
  """Read the file content."""
105
110
  try:
106
111
  if self.is_binary:
107
- return await asyncio.to_thread(self.path.read_bytes)
108
- return await asyncio.to_thread(self.path.read_text)
112
+ return await anyio.to_thread.run_sync(self.path.read_bytes)
113
+ return await anyio.to_thread.run_sync(self.path.read_text)
109
114
  except Exception as e:
110
115
  raise ValueError(f"Error reading file {self.path}: {e}")
111
116
 
@@ -114,11 +119,11 @@ class HttpResource(Resource):
114
119
  """A resource that reads from an HTTP endpoint."""
115
120
 
116
121
  url: str = Field(description="URL to fetch content from")
117
- mime_type: str | None = Field(
122
+ mime_type: str = Field(
118
123
  default="application/json", description="MIME type of the resource content"
119
124
  )
120
125
 
121
- async def read(self) -> Union[str, bytes]:
126
+ async def read(self) -> str | bytes:
122
127
  """Read the HTTP content."""
123
128
  async with httpx.AsyncClient() as client:
124
129
  response = await client.get(self.url)
@@ -136,7 +141,7 @@ class DirectoryResource(Resource):
136
141
  pattern: str | None = Field(
137
142
  default=None, description="Optional glob pattern to filter files"
138
143
  )
139
- mime_type: str | None = Field(
144
+ mime_type: str = Field(
140
145
  default="application/json", description="MIME type of the resource content"
141
146
  )
142
147
 
@@ -173,7 +178,7 @@ class DirectoryResource(Resource):
173
178
  async def read(self) -> str: # Always returns JSON string
174
179
  """Read the directory listing."""
175
180
  try:
176
- files = await asyncio.to_thread(self.list_files)
181
+ files = await anyio.to_thread.run_sync(self.list_files)
177
182
  file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
178
183
  return json.dumps({"files": file_list}, indent=2)
179
184
  except Exception as e:
@@ -0,0 +1,5 @@
1
+ from .server import FastMCP
2
+ from .context import Context
3
+
4
+
5
+ __all__ = ["FastMCP", "Context"]