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.
- Pyjo/MCP/Client.py +840 -0
- Pyjo/MCP/Prompt.py +260 -0
- Pyjo/MCP/Resource.py +205 -0
- Pyjo/MCP/ResourceTemplate.py +276 -0
- Pyjo/MCP/Server.py +1526 -0
- Pyjo/MCP/Test.py +761 -0
- Pyjo/MCP/Tool.py +399 -0
- Pyjo/MCP/__init__.py +80 -0
- pyjo_mcp-0.1.0.dist-info/METADATA +193 -0
- pyjo_mcp-0.1.0.dist-info/RECORD +12 -0
- pyjo_mcp-0.1.0.dist-info/WHEEL +4 -0
- pyjo_mcp-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -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
|