appmcp 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.
appmcp/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ from .application import AppMCP
2
+ from .context import MCPContext
3
+ from .decorators import prompt, resource, tool
4
+ from .dependencies import Depends
5
+ from .security import BearerAuth, Identity, OAuthAuth
6
+
7
+ __version__ = "0.1.0"
8
+ __all__ = [
9
+ "AppMCP",
10
+ "MCPContext",
11
+ "tool",
12
+ "resource",
13
+ "prompt",
14
+ "Depends",
15
+ "BearerAuth",
16
+ "OAuthAuth",
17
+ "Identity",
18
+ ]
@@ -0,0 +1,5 @@
1
+ from .base import MCPBackend
2
+ from .fastmcp import FastMCPAdapter, FastMCPBackend
3
+ from .official_sdk import OfficialSDKBackend
4
+
5
+ __all__ = ["MCPBackend", "FastMCPAdapter", "FastMCPBackend", "OfficialSDKBackend"]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol
4
+
5
+ from ..definitions import Definition
6
+
7
+
8
+ class MCPBackend(Protocol):
9
+ def add_tool(self, definition: Definition) -> None: ...
10
+ def add_resource(self, definition: Definition) -> None: ...
11
+ def add_prompt(self, definition: Definition) -> None: ...
12
+ def create_asgi_app(self, path: str = "/", **options: Any) -> Any: ...
13
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any: ...
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..definitions import Definition
6
+
7
+
8
+ class FastMCPBackend:
9
+ """FastMCP 3 adapter. FastMCP is imported only when this backend is created."""
10
+
11
+ def __init__(self, name: str, **options: Any) -> None:
12
+ try:
13
+ from fastmcp import FastMCP
14
+ except ImportError as exc:
15
+ raise ImportError("FastMCP is required: pip install appmcp") from exc
16
+ self.server = FastMCP(name=name, **options)
17
+
18
+ def add_tool(self, definition: Definition) -> None:
19
+ kwargs = {"name": definition.name, "description": definition.description}
20
+ if definition.read_only:
21
+ kwargs["annotations"] = {"readOnlyHint": True}
22
+ self.server.tool(**kwargs)(definition.callable)
23
+
24
+ def add_resource(self, definition: Definition) -> None:
25
+ self.server.resource(
26
+ definition.uri, name=definition.name, description=definition.description
27
+ )(definition.callable)
28
+
29
+ def add_prompt(self, definition: Definition) -> None:
30
+ self.server.prompt(name=definition.name, description=definition.description)(
31
+ definition.callable
32
+ )
33
+
34
+ def create_asgi_app(self, path: str = "/", **options: Any) -> Any:
35
+ return self.server.http_app(path=path, **options)
36
+
37
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
38
+ return await self.server.call_tool(name, arguments)
39
+
40
+
41
+ FastMCPAdapter = FastMCPBackend
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..definitions import Definition
6
+
7
+
8
+ class OfficialSDKBackend:
9
+ """Adapter for the official SDK's FastMCP compatibility surface.
10
+
11
+ Kept separate from the default FastMCP 3 engine so applications can opt in
12
+ and compatibility changes remain contained when official SDK v2 stabilizes.
13
+ """
14
+
15
+ def __init__(self, name: str, **options: Any):
16
+ try:
17
+ from mcp.server.fastmcp import FastMCP
18
+ except ImportError as exc:
19
+ raise ImportError("OfficialSDKBackend requires the 'mcp' Python package") from exc
20
+ self.server = FastMCP(name, **options)
21
+
22
+ def add_tool(self, definition: Definition) -> None:
23
+ self.server.tool(name=definition.name, description=definition.description)(
24
+ definition.callable
25
+ )
26
+
27
+ def add_resource(self, definition: Definition) -> None:
28
+ self.server.resource(
29
+ definition.uri, name=definition.name, description=definition.description
30
+ )(definition.callable)
31
+
32
+ def add_prompt(self, definition: Definition) -> None:
33
+ self.server.prompt(name=definition.name, description=definition.description)(
34
+ definition.callable
35
+ )
36
+
37
+ def create_asgi_app(self, path: str = "/", **options: Any) -> Any:
38
+ if hasattr(self.server, "streamable_http_app"):
39
+ return self.server.streamable_http_app()
40
+ if hasattr(self.server, "http_app"):
41
+ return self.server.http_app(path=path)
42
+ raise RuntimeError("Installed official MCP SDK does not provide an ASGI HTTP transport")
43
+
44
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
45
+ manager = getattr(self.server, "_tool_manager", None)
46
+ if manager is None:
47
+ raise RuntimeError("Installed official MCP SDK has no in-process tool manager")
48
+ return await manager.call_tool(name, arguments)
appmcp/application.py ADDED
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import inspect
5
+ import logging
6
+ import uuid
7
+ from collections.abc import Callable
8
+ from typing import Any, get_type_hints
9
+
10
+ from .audit import AuditLogger
11
+ from .context import MCPContext
12
+ from .decorators import prompt as prompt_decorator
13
+ from .decorators import resource as resource_decorator
14
+ from .decorators import tool as tool_decorator
15
+ from .definitions import Definition
16
+ from .dependencies import DependencyContainer, Depends
17
+ from .exceptions import AppMCPError
18
+ from .integrations.asgi import mount_asgi, request_scope
19
+ from .registry import Registry
20
+ from .security import InMemoryRateLimiter, SecurityPolicy
21
+ from .sessions import MemorySessionStore
22
+ from .settings import Settings
23
+ from .telemetry import Telemetry
24
+
25
+
26
+ class AppMCP:
27
+ def __init__(
28
+ self,
29
+ app: Any = None,
30
+ *,
31
+ name: str = "AppMCP",
32
+ path: str = "/mcp",
33
+ auth: Any = None,
34
+ default_deny: bool = False,
35
+ expose_errors: bool = False,
36
+ backend: Any = None,
37
+ session_store: Any = None,
38
+ audit: AuditLogger | None = None,
39
+ middleware: list[Callable] | None = None,
40
+ telemetry: Telemetry | None = None,
41
+ auto_mount: bool = True,
42
+ ) -> None:
43
+ self.name = name
44
+ self.settings = Settings(path=path, default_deny=default_deny, expose_errors=expose_errors)
45
+ self.app = app
46
+ self.auth = auth
47
+ self.registry = Registry()
48
+ self.dependencies = DependencyContainer()
49
+ self.policy = SecurityPolicy(default_deny=default_deny)
50
+ self.rate_limiter = InMemoryRateLimiter()
51
+ self.session_store = session_store or MemorySessionStore()
52
+ self.audit = audit or AuditLogger()
53
+ self.middleware = list(middleware or [])
54
+ self.telemetry = telemetry or Telemetry()
55
+ self.backend = backend
56
+ self._mounted_app = None
57
+ if app is not None and auto_mount:
58
+ self.mount(app, path=path)
59
+
60
+ @classmethod
61
+ def from_app(cls, app: Any, **kwargs: Any) -> AppMCP:
62
+ return cls(app, **kwargs)
63
+
64
+ @staticmethod
65
+ def depends(key: str) -> Depends:
66
+ return Depends(key)
67
+
68
+ def provide(self, key: str, provider: Any) -> AppMCP:
69
+ self.dependencies.provide(key, provider)
70
+ return self
71
+
72
+ def _register(self, target: Any, namespace: str | None = None) -> Definition:
73
+ definition = self.registry.register(target, namespace)
74
+ definition.callable = self._transport_callable(definition)
75
+ if self.backend is not None:
76
+ getattr(self.backend, f"add_{definition.kind}")(definition)
77
+ return definition
78
+
79
+ def tool(self, *args: Any, **kwargs: Any):
80
+ def register(fn):
81
+ tool_decorator(*args, **kwargs)(fn)
82
+ self._register(fn)
83
+ return fn
84
+
85
+ return register
86
+
87
+ def resource(self, uri: str, **kwargs: Any):
88
+ def register(fn):
89
+ resource_decorator(uri, **kwargs)(fn)
90
+ self._register(fn)
91
+ return fn
92
+
93
+ return register
94
+
95
+ def prompt(self, *args: Any, **kwargs: Any):
96
+ def register(fn):
97
+ prompt_decorator(*args, **kwargs)(fn)
98
+ self._register(fn)
99
+ return fn
100
+
101
+ return register
102
+
103
+ def include(self, instance: Any, namespace: str | None = None) -> AppMCP:
104
+ for definition in self.registry.include(instance, namespace):
105
+ definition.callable = self._transport_callable(definition)
106
+ if self.backend is not None:
107
+ getattr(self.backend, f"add_{definition.kind}")(definition)
108
+ return self
109
+
110
+ include_instance = include
111
+
112
+ def use(self, middleware: Callable) -> AppMCP:
113
+ self.middleware.append(middleware)
114
+ return self
115
+
116
+ def _ensure_backend(self):
117
+ if self.backend is None:
118
+ from .adapters.fastmcp import FastMCPBackend
119
+
120
+ self.backend = FastMCPBackend(self.name)
121
+ for definition in self.registry.all():
122
+ getattr(self.backend, f"add_{definition.kind}")(definition)
123
+ return self.backend
124
+
125
+ def mount(self, app: Any = None, *, path: str | None = None) -> Any:
126
+ app = app or self.app
127
+ if app is None:
128
+ raise ValueError("mount() requires an ASGI application")
129
+ self.app = app
130
+ path = path or self.settings.path
131
+ mcp_app = self._ensure_backend().create_asgi_app(
132
+ path="/",
133
+ json_response=self.settings.json_response,
134
+ stateless_http=self.settings.stateless_http,
135
+ )
136
+ self._mounted_app = mount_asgi(app, mcp_app, path)
137
+ return self._mounted_app
138
+
139
+ def asgi_app(self, *, path: str = "/") -> Any:
140
+ return self._ensure_backend().create_asgi_app(
141
+ path=path,
142
+ json_response=self.settings.json_response,
143
+ stateless_http=self.settings.stateless_http,
144
+ )
145
+
146
+ async def _context(
147
+ self, *, headers=None, user=None, scopes=(), confirmed=False, session=None, metadata=None
148
+ ) -> MCPContext:
149
+ scope = request_scope.get() or {}
150
+ raw_headers = scope.get("headers", ())
151
+ scope_headers = {k.decode("latin1"): v.decode("latin1") for k, v in raw_headers}
152
+ headers = {**scope_headers, **(headers or {})}
153
+ if self.auth is not None and user is None:
154
+ identity = await self.auth.authenticate(headers)
155
+ if identity:
156
+ user, scopes = identity.user, identity.scopes
157
+ request_id = next(
158
+ (v for k, v in headers.items() if k.lower() == "x-request-id"), str(uuid.uuid4())
159
+ )
160
+ return MCPContext(
161
+ user=user,
162
+ headers=headers,
163
+ request_id=request_id,
164
+ session=session,
165
+ app=self.app,
166
+ services=self.dependencies,
167
+ logger=logging.getLogger(f"appmcp.{self.name}"),
168
+ scopes=frozenset(scopes),
169
+ confirmed=confirmed,
170
+ metadata=metadata or {},
171
+ )
172
+
173
+ async def call_tool(self, name: str, arguments: dict[str, Any], **context: Any) -> Any:
174
+ return await self.call("tool", name, arguments, **context)
175
+
176
+ async def call(self, kind: str, name: str, arguments: dict[str, Any], **context: Any) -> Any:
177
+ definition = self.registry.get(kind, name)
178
+ ctx = await self._context(**context)
179
+ await self.policy.check(definition, ctx)
180
+ await self.rate_limiter.check(definition, ctx)
181
+ kwargs = dict(arguments)
182
+ signature = inspect.signature(
183
+ definition.metadata.get("original_callable", definition.callable)
184
+ )
185
+ try:
186
+ hints = get_type_hints(
187
+ definition.metadata.get("original_callable", definition.callable)
188
+ )
189
+ except (NameError, TypeError):
190
+ hints = {}
191
+ for parameter in signature.parameters.values():
192
+ if parameter.name in kwargs:
193
+ continue
194
+ annotation = hints.get(parameter.name, parameter.annotation)
195
+ if (
196
+ annotation is MCPContext
197
+ or parameter.name == "ctx"
198
+ and parameter.default is inspect.Parameter.empty
199
+ ):
200
+ kwargs[parameter.name] = ctx
201
+ elif isinstance(parameter.default, Depends):
202
+ kwargs[parameter.name] = await self.dependencies.resolve(parameter.default.key, ctx)
203
+
204
+ async def invoke():
205
+ value = definition.metadata["original_callable"](**kwargs)
206
+ return await value if inspect.isawaitable(value) else value
207
+
208
+ handler = invoke
209
+ for middleware in reversed(self.middleware):
210
+ next_handler = handler
211
+
212
+ async def handler(middleware=middleware, next_handler=next_handler):
213
+ value = middleware(definition, ctx, kwargs, next_handler)
214
+ return await value if inspect.isawaitable(value) else value
215
+
216
+ error = None
217
+ try:
218
+ with self.telemetry.span(f"appmcp.{kind}.{name}"):
219
+ return await handler()
220
+ except Exception as exc:
221
+ error = exc
222
+ if self.settings.expose_errors or isinstance(exc, AppMCPError):
223
+ raise
224
+ raise AppMCPError("Operation failed") from exc
225
+ finally:
226
+ await self.audit.record(name, ctx, arguments, error)
227
+
228
+ def _transport_callable(self, definition: Definition):
229
+ original = definition.callable
230
+ definition.metadata["original_callable"] = original
231
+ signature = inspect.signature(original)
232
+ try:
233
+ hints = get_type_hints(original)
234
+ except (NameError, TypeError):
235
+ hints = {}
236
+ public = [
237
+ p
238
+ for p in signature.parameters.values()
239
+ if p.annotation is not MCPContext
240
+ and not isinstance(p.default, Depends)
241
+ and p.name != "ctx"
242
+ ]
243
+ public = [p for p in public if hints.get(p.name, p.annotation) is not MCPContext]
244
+
245
+ @functools.wraps(original)
246
+ async def wrapper(**kwargs):
247
+ return await self.call(definition.kind, definition.name, kwargs)
248
+
249
+ wrapper.__signature__ = signature.replace(parameters=public) # type: ignore[attr-defined]
250
+ return wrapper
251
+
252
+ def test_client(self, **kwargs: Any):
253
+ from .testing import TestClient
254
+
255
+ return TestClient(self, **kwargs)
256
+
257
+ def inspect(self) -> dict[str, Any]:
258
+ return {
259
+ "name": self.name,
260
+ "path": self.settings.path,
261
+ "tools": [d.name for d in self.registry.all("tool")],
262
+ "resources": [{"name": d.name, "uri": d.uri} for d in self.registry.all("resource")],
263
+ "prompts": [d.name for d in self.registry.all("prompt")],
264
+ }
appmcp/audit.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import logging
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timezone
8
+ from typing import Any
9
+
10
+
11
+ @dataclass
12
+ class AuditEvent:
13
+ operation: str
14
+ user: Any
15
+ arguments: dict[str, Any]
16
+ success: bool
17
+ error: str | None
18
+ timestamp: datetime
19
+
20
+
21
+ class AuditLogger:
22
+ def __init__(self, sink: Callable[[AuditEvent], Any] | None = None) -> None:
23
+ self.sink = sink
24
+
25
+ async def record(
26
+ self,
27
+ operation: str,
28
+ context: Any,
29
+ arguments: dict[str, Any],
30
+ error: Exception | None = None,
31
+ ) -> None:
32
+ event = AuditEvent(
33
+ operation,
34
+ context.user,
35
+ arguments,
36
+ error is None,
37
+ str(error) if error else None,
38
+ datetime.now(timezone.utc),
39
+ )
40
+ if self.sink:
41
+ result = self.sink(event)
42
+ if inspect.isawaitable(result):
43
+ await result
44
+ else:
45
+ logging.getLogger("appmcp.audit").info("%s", event)
appmcp/cli.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import importlib
5
+ import json
6
+ from typing import Any
7
+
8
+ from .application import AppMCP
9
+ from .config import config_json
10
+
11
+
12
+ def load_object(reference: str) -> Any:
13
+ try:
14
+ module_name, object_path = reference.split(":", 1)
15
+ except ValueError as exc:
16
+ raise ValueError("Application reference must look like package.module:object") from exc
17
+ value = importlib.import_module(module_name)
18
+ for part in object_path.split("."):
19
+ value = getattr(value, part)
20
+ return value
21
+
22
+
23
+ def find_appmcp(value: Any) -> AppMCP:
24
+ if isinstance(value, AppMCP):
25
+ return value
26
+ for candidate in vars(importlib.import_module(value.__module__)).values():
27
+ if isinstance(candidate, AppMCP) and (
28
+ candidate.app is value or value is candidate._mounted_app
29
+ ):
30
+ return candidate
31
+ raise ValueError("No AppMCP instance is associated with that application")
32
+
33
+
34
+ def inspect_command(reference: str) -> int:
35
+ info = find_appmcp(load_object(reference)).inspect()
36
+ print(f"Application: {info['name']}\nMCP path: {info['path']}\n")
37
+ for heading, key in (("Tools", "tools"), ("Resources", "resources"), ("Prompts", "prompts")):
38
+ print(f"{heading}:")
39
+ for item in info[key]:
40
+ print(f" {item['uri'] if isinstance(item, dict) else item}")
41
+ return 0
42
+
43
+
44
+ def dev_command(reference: str, host: str, port: int) -> int:
45
+ try:
46
+ import uvicorn
47
+ except ImportError as exc:
48
+ raise SystemExit("Development server requires: pip install appmcp[fastapi]") from exc
49
+ uvicorn.run(reference, host=host, port=port, factory=False)
50
+ return 0
51
+
52
+
53
+ def test_command(reference: str) -> int:
54
+ mcp = find_appmcp(load_object(reference))
55
+ info = mcp.inspect()
56
+ print(json.dumps(info, indent=2))
57
+ print("Registration validation passed")
58
+ return 0
59
+
60
+
61
+ def main(argv: list[str] | None = None) -> int:
62
+ parser = argparse.ArgumentParser(prog="appmcp")
63
+ sub = parser.add_subparsers(dest="command", required=True)
64
+ inspect_parser = sub.add_parser("inspect", help="List exposed capabilities")
65
+ inspect_parser.add_argument("application")
66
+ dev = sub.add_parser("dev", help="Run an ASGI development server")
67
+ dev.add_argument("application")
68
+ dev.add_argument("--host", default="127.0.0.1")
69
+ dev.add_argument("--port", type=int, default=8000)
70
+ test = sub.add_parser("test", help="Validate capability registration")
71
+ test.add_argument("application")
72
+ config = sub.add_parser("config", help="Generate MCP client configuration")
73
+ config.add_argument("client", choices=["claude", "cursor", "vscode"])
74
+ config.add_argument("application")
75
+ config.add_argument("--url", default="http://127.0.0.1:8000/mcp")
76
+ args = parser.parse_args(argv)
77
+ if args.command == "inspect":
78
+ return inspect_command(args.application)
79
+ if args.command == "dev":
80
+ return dev_command(args.application, args.host, args.port)
81
+ if args.command == "test":
82
+ return test_command(args.application)
83
+ mcp = find_appmcp(load_object(args.application))
84
+ print(config_json(args.client, url=args.url, name=mcp.name))
85
+ return 0
86
+
87
+
88
+ if __name__ == "__main__":
89
+ raise SystemExit(main())
appmcp/config.py ADDED
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+
7
+ def generate_config(client: str, *, url: str, name: str = "appmcp") -> dict[str, Any]:
8
+ client = client.lower()
9
+ server = {"url": url, "transport": "streamable-http"}
10
+ if client in {"claude", "cursor", "vscode"}:
11
+ return {"mcpServers": {name: server}}
12
+ raise ValueError(f"Unsupported client {client!r}; choose claude, cursor, or vscode")
13
+
14
+
15
+ def config_json(client: str, *, url: str, name: str = "appmcp") -> str:
16
+ return json.dumps(generate_config(client, url=url, name=name), indent=2)
appmcp/context.py ADDED
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class MCPContext:
11
+ user: Any = None
12
+ headers: Mapping[str, str] = field(default_factory=dict)
13
+ request_id: str = ""
14
+ session: Any = None
15
+ app: Any = None
16
+ services: Any = None
17
+ logger: logging.Logger = field(default_factory=lambda: logging.getLogger("appmcp"))
18
+ scopes: frozenset[str] = field(default_factory=frozenset)
19
+ confirmed: bool = False
20
+ metadata: dict[str, Any] = field(default_factory=dict)
appmcp/decorators.py ADDED
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any, TypeVar
5
+
6
+ from .definitions import Exposure
7
+
8
+ F = TypeVar("F", bound=Callable[..., Any])
9
+ MARKER = "__appmcp_exposure__"
10
+
11
+
12
+ def _decorate(kind: str, **options: Any):
13
+ exposure = Exposure(kind=kind, **options)
14
+
15
+ def wrapper(fn: F) -> F:
16
+ setattr(fn, MARKER, exposure)
17
+ return fn
18
+
19
+ return wrapper
20
+
21
+
22
+ def tool(
23
+ name: str | None = None,
24
+ *,
25
+ description: str | None = None,
26
+ scopes: list[str] | tuple[str, ...] = (),
27
+ rate_limit: str | None = None,
28
+ read_only: bool = False,
29
+ confirmation_required: bool = False,
30
+ enabled: bool | Callable[[Any], bool] = True,
31
+ **metadata: Any,
32
+ ):
33
+ return _decorate(
34
+ "tool",
35
+ name=name,
36
+ description=description,
37
+ scopes=tuple(scopes),
38
+ rate_limit=rate_limit,
39
+ read_only=read_only,
40
+ confirmation_required=confirmation_required,
41
+ enabled=enabled,
42
+ metadata=metadata,
43
+ )
44
+
45
+
46
+ def resource(
47
+ uri: str,
48
+ *,
49
+ name: str | None = None,
50
+ description: str | None = None,
51
+ scopes: list[str] | tuple[str, ...] = (),
52
+ enabled: bool | Callable[[Any], bool] = True,
53
+ **metadata: Any,
54
+ ):
55
+ return _decorate(
56
+ "resource",
57
+ name=name,
58
+ uri=uri,
59
+ description=description,
60
+ scopes=tuple(scopes),
61
+ enabled=enabled,
62
+ metadata=metadata,
63
+ )
64
+
65
+
66
+ def prompt(
67
+ name: str | None = None,
68
+ *,
69
+ description: str | None = None,
70
+ scopes: list[str] | tuple[str, ...] = (),
71
+ enabled: bool | Callable[[Any], bool] = True,
72
+ **metadata: Any,
73
+ ):
74
+ return _decorate(
75
+ "prompt",
76
+ name=name,
77
+ description=description,
78
+ scopes=tuple(scopes),
79
+ enabled=enabled,
80
+ metadata=metadata,
81
+ )
82
+
83
+
84
+ def get_exposure(value: Any) -> Exposure | None:
85
+ return getattr(value, MARKER, None) or getattr(getattr(value, "__func__", None), MARKER, None)
appmcp/definitions.py ADDED
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Exposure:
10
+ kind: str
11
+ name: str | None = None
12
+ description: str | None = None
13
+ uri: str | None = None
14
+ scopes: tuple[str, ...] = ()
15
+ rate_limit: str | None = None
16
+ read_only: bool = False
17
+ confirmation_required: bool = False
18
+ enabled: bool | Callable[[Any], bool] = True
19
+ metadata: dict[str, Any] = field(default_factory=dict)
20
+
21
+
22
+ @dataclass
23
+ class Definition:
24
+ kind: str
25
+ name: str
26
+ callable: Callable[..., Any]
27
+ description: str = ""
28
+ uri: str | None = None
29
+ scopes: tuple[str, ...] = ()
30
+ rate_limit: str | None = None
31
+ read_only: bool = False
32
+ confirmation_required: bool = False
33
+ enabled: bool | Callable[[Any], bool] = True
34
+ metadata: dict[str, Any] = field(default_factory=dict)
35
+
36
+
37
+ ToolDefinition = Definition
38
+ ResourceDefinition = Definition
39
+ PromptDefinition = Definition