Pyjo-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,276 @@
1
+ """
2
+ Pyjo.MCP.ResourceTemplate - Resource template of a server of MCP
3
+ ================================================================
4
+ ::
5
+
6
+ from Pyjo.MCP.ResourceTemplate import ResourceTemplate
7
+
8
+ def note(number: str) -> str:
9
+ \"\"\"A note, by its number.\"\"\"
10
+ return f'Note {number}'
11
+
12
+ template = ResourceTemplate('notes://{number}', note)
13
+ print(template.uri, template.name, template.description)
14
+
15
+ # Or import the module, as in Google Python Style Guide
16
+
17
+ import Pyjo.MCP.ResourceTemplate
18
+
19
+ template = Pyjo.MCP.ResourceTemplate.new('file:///project/{+path}', lambda path: path.upper())
20
+ template.resource('file:///project/README.md').read_p().then(print).wait()
21
+
22
+ :mod:`Pyjo.MCP.ResourceTemplate` is a resource template of
23
+ :mod:`Pyjo.MCP.Server`, the resources of the URIs that a URI Template of
24
+ :rfc:`6570` of :mod:`Pyjo.URL.Template` matches, which a client reads by their
25
+ URIs. The values of the variables of a URI are the keyword arguments of the
26
+ function, which returns the contents of the resource, as the function of a
27
+ :mod:`Pyjo.MCP.Resource` does. The name and the description of the resource
28
+ template are those of the function by default: its name and the first
29
+ paragraph of its docstring.
30
+
31
+ The values of the variables come from the client, so a URI whose value has a
32
+ segment ``..``, an absolute path or the character NUL, which would read a
33
+ file outside of a directory of the function, has no resource, unless the
34
+ resource template is not :attr:`~Pyjo_MCP_ResourceTemplate.safe`.
35
+
36
+ See also
37
+ --------
38
+
39
+ :mod:`Pyjo.MCP.Resource`, :mod:`Pyjo.MCP.Server`, :mod:`Pyjo.URL.Template`,
40
+ :mod:`Pyjo.MCP`.
41
+
42
+ Classes
43
+ -------
44
+ """
45
+
46
+ import Pyjo.Base
47
+ import Pyjo.URL.Template
48
+
49
+ from . import Resource
50
+
51
+ import functools
52
+ import inspect
53
+ import re
54
+
55
+ from collections.abc import Callable, Mapping
56
+ from typing import Any, cast
57
+
58
+
59
+ class Pyjo_MCP_ResourceTemplate(Pyjo.Base.object):
60
+ """
61
+ :mod:`Pyjo.MCP.ResourceTemplate` inherits all methods from
62
+ :mod:`Pyjo.Base` and implements the following new ones.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ uri: str,
68
+ cb: Callable[..., Any],
69
+ *,
70
+ name: str | None = None,
71
+ title: str | None = None,
72
+ description: str | None = None,
73
+ mime_type: str = 'text/plain',
74
+ context: bool = False,
75
+ safe: bool = True,
76
+ complete: Mapping[str, Any] | None = None,
77
+ ) -> None:
78
+ """::
79
+
80
+ template = Pyjo.MCP.ResourceTemplate.new('notes://{number}', note)
81
+ template = Pyjo.MCP.ResourceTemplate.new('file:///project/{+path}', read, mime_type='text/markdown')
82
+ template = Pyjo.MCP.ResourceTemplate.new('users://{id}', lambda context, id: ..., context=True)
83
+ template = Pyjo.MCP.ResourceTemplate.new('notes://{number}', note, complete={'number': ['1', '2']})
84
+
85
+ Construct a new :mod:`Pyjo.MCP.ResourceTemplate` object of a URI
86
+ Template and a function. With ``context``, the function gets the
87
+ :class:`~Pyjo.MCP.Server.Context` of the request before the values of
88
+ the variables. A URI Template that is not valid, or whose expressions
89
+ cannot be told apart, raises a :mod:`Pyjo.Exception`.
90
+ """
91
+ if description is None:
92
+ doc = inspect.getdoc(cb) or ''
93
+ description = doc.split('\n\n', 1)[0].replace('\n', ' ').strip() or None
94
+
95
+ self.cb = cb
96
+ """::
97
+
98
+ cb = template.cb
99
+
100
+ The function of the resource template.
101
+ """
102
+
103
+ self.complete: dict[str, Any] = dict(complete or {})
104
+ """::
105
+
106
+ completions = template.complete
107
+
108
+ The completions of the values of the variables, by their names, which
109
+ ``completion/complete`` gives: a list of the values, which the server
110
+ filters by what the user has typed, or a function that gets it, and the
111
+ values of the other variables, after the context of the request with
112
+ ``context``, and returns the values, or a :mod:`Pyjo.Promise` of them. ::
113
+
114
+ template.complete['number'] = lambda value, arguments: [str(n) for n in range(1, len(notes) + 1)]
115
+ """
116
+
117
+ self.context = context
118
+ """::
119
+
120
+ boolean = template.context
121
+
122
+ Whether the function gets the context of the request.
123
+ """
124
+
125
+ self.description = description
126
+ """::
127
+
128
+ description = template.description
129
+
130
+ What the resources are, the first paragraph of the docstring of the
131
+ function by default, or ``None``.
132
+ """
133
+
134
+ self.mime_type = mime_type
135
+ """::
136
+
137
+ type = template.mime_type
138
+
139
+ The media type of the contents, ``text/plain`` by default.
140
+ """
141
+
142
+ self.name = name or cb.__name__
143
+ """::
144
+
145
+ name = template.name
146
+
147
+ The name of the resource template, the one of the function by default.
148
+ """
149
+
150
+ self.safe = safe
151
+ """::
152
+
153
+ boolean = template.safe
154
+
155
+ Whether a URI whose value has a segment ``..``, an absolute path or the
156
+ character NUL has no resource, true by default.
157
+ """
158
+
159
+ self.template = Pyjo.URL.Template.new(uri)
160
+ """::
161
+
162
+ tpl = template.template
163
+
164
+ The :mod:`Pyjo.URL.Template` object of the URI Template.
165
+ """
166
+ # A template whose expressions cannot be told apart raises here, not at the first read
167
+ self.template.match('')
168
+
169
+ self.title = title
170
+ """::
171
+
172
+ title = template.title
173
+
174
+ The name of the resource template for people, or ``None``.
175
+ """
176
+
177
+ self.uri = uri
178
+ """::
179
+
180
+ uri = template.uri
181
+
182
+ The URI Template of the resources, which ``resources/templates/list``
183
+ gives as ``uriTemplate``.
184
+ """
185
+
186
+ def __json__(self) -> dict[str, Any]:
187
+ """::
188
+
189
+ definition = template.__json__()
190
+
191
+ The definition of the resource template, as
192
+ ``resources/templates/list`` gives it, with its ``uriTemplate``,
193
+ ``name``, ``title``, ``description`` and ``mimeType``, where it has
194
+ them.
195
+ """
196
+ definition: dict[str, Any] = {'uriTemplate': self.uri, 'name': self.name}
197
+ if self.title is not None:
198
+ definition['title'] = self.title
199
+ if self.description is not None:
200
+ definition['description'] = self.description
201
+ definition['mimeType'] = self.mime_type
202
+ return definition
203
+
204
+ def match(self, uri: str) -> dict[str, Any] | None:
205
+ """::
206
+
207
+ variables = template.match('notes://1')
208
+
209
+ The values of the variables of a URI, as
210
+ :meth:`~Pyjo.URL.Template.Pyjo_URL_Template.match` of the template
211
+ gives them, or ``None`` for a URI that the template does not match, or
212
+ whose value is not safe. ::
213
+
214
+ files = Pyjo.MCP.ResourceTemplate.new('file:///project/{+path}', lambda path: '')
215
+
216
+ # {'path': 'docs/index.md'}
217
+ files.match('file:///project/docs/index.md')
218
+
219
+ # None
220
+ files.match('file:///project/../etc/passwd')
221
+ """
222
+ variables = self.template.match(uri)
223
+ if variables is None or (self.safe and not all(map(_safe, variables.values()))):
224
+ return None
225
+ return variables
226
+
227
+ def resource(self, uri: str) -> Resource.object | None:
228
+ """::
229
+
230
+ resource = template.resource('notes://1')
231
+
232
+ The :mod:`Pyjo.MCP.Resource` of a URI, whose function is the one of
233
+ the resource template with the values of the variables of the URI, or
234
+ ``None`` for a URI that :meth:`match` does not match, or whose values
235
+ the function does not take, as without a value of a variable that the
236
+ function requires. ::
237
+
238
+ # {'contents': [{'uri': 'notes://1', 'mimeType': 'text/plain', 'text': 'Note 1'}]}
239
+ Pyjo.MCP.ResourceTemplate.new('notes://{number}', note).resource('notes://1').read_p().then(print).wait()
240
+ """
241
+ if (variables := self.match(uri)) is None:
242
+ return None
243
+ try:
244
+ inspect.signature(self.cb).bind(*([None] if self.context else []), **variables)
245
+ except TypeError:
246
+ return None
247
+ except ValueError:
248
+ # A function without a signature, as some builtins, takes what it takes
249
+ pass
250
+ return Resource.new(
251
+ uri,
252
+ functools.partial(self.cb, **variables),
253
+ name=self.name,
254
+ title=self.title,
255
+ description=self.description,
256
+ mime_type=self.mime_type,
257
+ context=self.context,
258
+ )
259
+
260
+
261
+ # A segment .. or an absolute path of Unix or of Windows
262
+ _UNSAFE = re.compile(r'(?:^|[/\\])\.\.(?:[/\\]|$)|^[/\\]|^[A-Za-z]:')
263
+
264
+
265
+ def _safe(value: Any) -> bool:
266
+ # Whether a value of a variable, a string, a list or a dict of them, has no segment .., no absolute path and no NUL
267
+ if isinstance(value, dict):
268
+ return all(map(_safe, cast(dict[str, Any], value).values()))
269
+ if isinstance(value, list):
270
+ return all(map(_safe, cast(list[Any], value)))
271
+ return '\0' not in value and not _UNSAFE.search(value)
272
+
273
+
274
+ new = Pyjo_MCP_ResourceTemplate.new
275
+ object = Pyjo_MCP_ResourceTemplate
276
+ ResourceTemplate = Pyjo_MCP_ResourceTemplate