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/Prompt.py ADDED
@@ -0,0 +1,260 @@
1
+ """
2
+ Pyjo.MCP.Prompt - Prompt of a server of MCP
3
+ ===========================================
4
+ ::
5
+
6
+ from Pyjo.MCP.Prompt import Prompt
7
+
8
+ def review(code: str, language: str = 'Python') -> str:
9
+ \"\"\"Review a piece of code.
10
+
11
+ :param code: The code to review.
12
+ :param language: The language of the code.
13
+ \"\"\"
14
+ return f'Review this {language} code:\\n{code}'
15
+
16
+ prompt = Prompt(review)
17
+ print(prompt.name, prompt.description, prompt.arguments)
18
+
19
+ # Or import the module, as in Google Python Style Guide
20
+
21
+ import Pyjo.MCP.Prompt
22
+
23
+ prompt = Pyjo.MCP.Prompt.new(review, name='code_review', title='Code review')
24
+ prompt.get_p({'code': 'x = 1'}).then(print).wait()
25
+
26
+ :mod:`Pyjo.MCP.Prompt` is a prompt of :mod:`Pyjo.MCP.Server`, a template of
27
+ messages for a language model, which a user picks and fills with its
28
+ arguments. The name, the description and the arguments of the prompt are those
29
+ of the function by default: its name, the first paragraph of its docstring,
30
+ and its parameters, with the descriptions of its docstring, of which those
31
+ without a default are required.
32
+
33
+ The function returns the messages of the prompt, as a value, a
34
+ :mod:`Pyjo.Promise` of it or a coroutine, of an ``async def``:
35
+
36
+ * a string is one message of the user with the text;
37
+ * a :class:`list` is the messages, each with its ``role`` and its
38
+ ``content``;
39
+ * a :class:`dict` with ``messages`` is the result as it is, as with a
40
+ ``description`` of its own.
41
+
42
+ A missing argument that is required, or one that the function does not take,
43
+ is an error of the request, ``-32602``, as the specification requires, and an
44
+ exception of the function an error of the server.
45
+
46
+ See also
47
+ --------
48
+
49
+ :mod:`Pyjo.MCP.Server`, :mod:`Pyjo.MCP.Resource`, :mod:`Pyjo.MCP.Tool`,
50
+ :mod:`Pyjo.MCP`.
51
+
52
+ Classes
53
+ -------
54
+ """
55
+
56
+ import Pyjo.Base
57
+ import Pyjo.JSON.RPC
58
+ import Pyjo.JSON.Schema
59
+ import Pyjo.Promise
60
+
61
+ import inspect
62
+
63
+ from collections.abc import Callable, Mapping
64
+ from typing import Any, cast
65
+
66
+
67
+ class Pyjo_MCP_Prompt(Pyjo.Base.object):
68
+ """
69
+ :mod:`Pyjo.MCP.Prompt` inherits all methods from :mod:`Pyjo.Base` and
70
+ implements the following new ones.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ cb: Callable[..., Any],
76
+ *,
77
+ name: str | None = None,
78
+ title: str | None = None,
79
+ description: str | None = None,
80
+ arguments: list[Mapping[str, Any]] | None = None,
81
+ context: bool = False,
82
+ complete: Mapping[str, Any] | None = None,
83
+ ) -> None:
84
+ """::
85
+
86
+ prompt = Pyjo.MCP.Prompt.new(review)
87
+ prompt = Pyjo.MCP.Prompt.new(review, name='code_review', title='Code review')
88
+ prompt = Pyjo.MCP.Prompt.new(lambda **args: ..., arguments=[{'name': 'code', 'required': True}])
89
+ prompt = Pyjo.MCP.Prompt.new(lambda context, code: ..., context=True)
90
+ prompt = Pyjo.MCP.Prompt.new(review, complete={'language': ['Perl', 'Python']})
91
+
92
+ Construct a new :mod:`Pyjo.MCP.Prompt` object of a function. With
93
+ ``context``, the function gets the :class:`~Pyjo.MCP.Server.Context` of
94
+ the request before its arguments.
95
+ """
96
+ skip = 1 if context else 0
97
+ completions: dict[str, Any] = {}
98
+ if arguments is None:
99
+ data = cast(dict[str, Any], Pyjo.JSON.Schema.parameters(cb, skip=skip).data)
100
+ if description is None:
101
+ description = data.get('description')
102
+ required = cast(list[str], data.get('required', []))
103
+ arguments = []
104
+ for key, property in cast(dict[str, Any], data.get('properties', {})).items():
105
+ argument: dict[str, Any] = {'name': key}
106
+ if (text := cast(dict[str, Any], property).get('description')) is not None:
107
+ argument['description'] = text
108
+ argument['required'] = key in required
109
+ arguments.append(argument)
110
+ if isinstance(values := cast(dict[str, Any], property).get('enum'), list):
111
+ completions[key] = [str(value) for value in cast(list[Any], values)]
112
+
113
+ self.arguments: list[dict[str, Any]] = [dict(argument) for argument in arguments]
114
+ """::
115
+
116
+ arguments = prompt.arguments
117
+
118
+ The arguments of the prompt, each with its ``name``, its
119
+ ``description`` and whether it is ``required``, the parameters of the
120
+ function by default.
121
+ """
122
+
123
+ self.cb = cb
124
+ """::
125
+
126
+ cb = prompt.cb
127
+
128
+ The function of the prompt.
129
+ """
130
+
131
+ self.complete: dict[str, Any] = {**completions, **(complete or {})}
132
+ """::
133
+
134
+ completions = prompt.complete
135
+
136
+ The completions of the values of the arguments, by their names, which
137
+ ``completion/complete`` gives: a list of the values, which the server
138
+ filters by what the user has typed, or a function that gets it, and the
139
+ values of the other arguments, after the context of the request with
140
+ ``context``, and returns the values, or a :mod:`Pyjo.Promise` of them. The
141
+ values of a parameter with ``Literal`` or ``Enum`` complete by default. ::
142
+
143
+ prompt.complete['language'] = lambda value, arguments: ['Perl', 'Python']
144
+ """
145
+
146
+ self.context = context
147
+ """::
148
+
149
+ boolean = prompt.context
150
+
151
+ Whether the function gets the context of the request before its
152
+ arguments.
153
+ """
154
+
155
+ self.description = description or ''
156
+ """::
157
+
158
+ description = prompt.description
159
+
160
+ What the prompt is for, the first paragraph of the docstring of the
161
+ function by default.
162
+ """
163
+
164
+ self.name = name or cb.__name__
165
+ """::
166
+
167
+ name = prompt.name
168
+
169
+ The name of the prompt, the one of the function by default.
170
+ """
171
+
172
+ self.title = title
173
+ """::
174
+
175
+ title = prompt.title
176
+
177
+ The name of the prompt for people, or ``None``.
178
+ """
179
+
180
+ self._signature = inspect.signature(cb)
181
+
182
+ def __json__(self) -> dict[str, Any]:
183
+ """::
184
+
185
+ definition = prompt.__json__()
186
+
187
+ The definition of the prompt, as ``prompts/list`` gives it, with its
188
+ ``name``, ``title``, ``description`` and ``arguments``, where it has
189
+ them.
190
+ """
191
+ definition: dict[str, Any] = {'name': self.name}
192
+ if self.title is not None:
193
+ definition['title'] = self.title
194
+ definition['description'] = self.description
195
+ definition['arguments'] = self.arguments
196
+ return definition
197
+
198
+ def get_p(self, arguments: Mapping[str, Any], context: Any = None) -> Pyjo.Promise.object:
199
+ """::
200
+
201
+ promise = prompt.get_p({'code': 'x = 1'})
202
+ promise = prompt.get_p({'code': 'x = 1'}, context)
203
+
204
+ Call the function with the arguments, and return a :mod:`Pyjo.Promise`
205
+ of the result of the prompt, as ``prompts/get`` gives it, without its
206
+ ``resultType``, or rejected with a :class:`Pyjo.JSON.RPC.Error` of
207
+ arguments that are not valid, or an exception of the function. ::
208
+
209
+ # {'messages': [{'role': 'user', 'content': {'type': 'text', 'text': 'Review this Python code:\\nx = 1'}}]}
210
+ prompt.get_p({'code': 'x = 1'}).then(print).wait()
211
+ """
212
+ for argument in self.arguments:
213
+ if argument.get('required') and argument['name'] not in arguments:
214
+ message = f'Missing required argument: {argument["name"]}'
215
+ return Pyjo.Promise.reject(Pyjo.JSON.RPC.Error.new(Pyjo.JSON.RPC.INVALID_PARAMS, message))
216
+ try:
217
+ bound = self._signature.bind(*([context] if self.context else []), **arguments)
218
+ except TypeError as e:
219
+ message = f'Invalid arguments: {e}'
220
+ return Pyjo.Promise.reject(Pyjo.JSON.RPC.Error.new(Pyjo.JSON.RPC.INVALID_PARAMS, message))
221
+
222
+ try:
223
+ value = self.cb(*bound.args, **bound.kwargs)
224
+ except Exception as e:
225
+ return Pyjo.Promise.reject(e)
226
+ if inspect.iscoroutine(value):
227
+ value = Pyjo.Promise.new(value)
228
+ if isinstance(value, Pyjo.Promise.object):
229
+ return value.then(self.result)
230
+ return Pyjo.Promise.resolve(self.result(value))
231
+
232
+ def result(self, value: Any = None) -> dict[str, Any]:
233
+ """::
234
+
235
+ result = prompt.result('Review this code')
236
+ result = prompt.result([{'role': 'assistant', 'content': {'type': 'text', 'text': 'Done'}}])
237
+
238
+ The result of the prompt of what the function returns.
239
+ """
240
+ if isinstance(value, dict) and 'messages' in value:
241
+ return cast(dict[str, Any], value)
242
+ if isinstance(value, list):
243
+ return {'messages': cast(list[Any], value)}
244
+ return text_result('' if value is None else str(cast(Any, value)))
245
+
246
+
247
+ def text_result(text: str, role: str = 'user') -> dict[str, Any]:
248
+ """::
249
+
250
+ result = Pyjo.MCP.Prompt.text_result('Review this code')
251
+ result = Pyjo.MCP.Prompt.text_result('Here is the review', role='assistant')
252
+
253
+ A result of a prompt with one message of the text, of the user by default.
254
+ """
255
+ return {'messages': [{'role': role, 'content': {'type': 'text', 'text': text}}]}
256
+
257
+
258
+ new = Pyjo_MCP_Prompt.new
259
+ object = Pyjo_MCP_Prompt
260
+ Prompt = Pyjo_MCP_Prompt
Pyjo/MCP/Resource.py ADDED
@@ -0,0 +1,205 @@
1
+ """
2
+ Pyjo.MCP.Resource - Resource of a server of MCP
3
+ ===============================================
4
+ ::
5
+
6
+ from Pyjo.MCP.Resource import Resource
7
+
8
+ def readme() -> str:
9
+ \"\"\"The documentation of the project.\"\"\"
10
+ return '# Pyjo-MCP'
11
+
12
+ resource = Resource('file:///project/README.md', readme, mime_type='text/markdown')
13
+ print(resource.uri, resource.name, resource.description)
14
+
15
+ # Or import the module, as in Google Python Style Guide
16
+
17
+ import Pyjo.MCP.Resource
18
+
19
+ resource = Pyjo.MCP.Resource.new('file:///project/logo.png', lambda: b'\\x89PNG', mime_type='image/png')
20
+ resource.read_p().then(print).wait()
21
+
22
+ :mod:`Pyjo.MCP.Resource` is a resource of :mod:`Pyjo.MCP.Server`, data that a
23
+ client reads by its URI, as the context of a language model. The name and the
24
+ description of the resource are those of the function by default: its name
25
+ and the first paragraph of its docstring.
26
+
27
+ The function returns the contents of the resource, as a value, a
28
+ :mod:`Pyjo.Promise` of it or a coroutine, of an ``async def``:
29
+
30
+ * a string is the ``text`` of the contents;
31
+ * :class:`bytes` are the ``blob`` of the contents, in Base64;
32
+ * a :class:`dict` with ``contents`` is the result as it is, as with several
33
+ of them;
34
+ * another value is the text of its JSON.
35
+
36
+ See also
37
+ --------
38
+
39
+ :mod:`Pyjo.MCP.Server`, :mod:`Pyjo.MCP.Prompt`, :mod:`Pyjo.MCP.Tool`,
40
+ :mod:`Pyjo.MCP`.
41
+
42
+ Classes
43
+ -------
44
+ """
45
+
46
+ import Pyjo.Base
47
+ import Pyjo.JSON
48
+ import Pyjo.Promise
49
+
50
+ import base64
51
+ import inspect
52
+
53
+ from collections.abc import Callable
54
+ from typing import Any, cast
55
+
56
+
57
+ class Pyjo_MCP_Resource(Pyjo.Base.object):
58
+ """
59
+ :mod:`Pyjo.MCP.Resource` inherits all methods from :mod:`Pyjo.Base` and
60
+ implements the following new ones.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ uri: str,
66
+ cb: Callable[..., Any],
67
+ *,
68
+ name: str | None = None,
69
+ title: str | None = None,
70
+ description: str | None = None,
71
+ mime_type: str = 'text/plain',
72
+ context: bool = False,
73
+ ) -> None:
74
+ """::
75
+
76
+ resource = Pyjo.MCP.Resource.new('file:///project/README.md', readme)
77
+ resource = Pyjo.MCP.Resource.new('file:///project/README.md', readme, mime_type='text/markdown')
78
+ resource = Pyjo.MCP.Resource.new('config://app', lambda context: ..., context=True)
79
+
80
+ Construct a new :mod:`Pyjo.MCP.Resource` object of a function. With
81
+ ``context``, the function gets the :class:`~Pyjo.MCP.Server.Context` of
82
+ the request.
83
+ """
84
+ if description is None:
85
+ doc = inspect.getdoc(cb) or ''
86
+ description = doc.split('\n\n', 1)[0].replace('\n', ' ').strip() or None
87
+
88
+ self.cb = cb
89
+ """::
90
+
91
+ cb = resource.cb
92
+
93
+ The function of the resource.
94
+ """
95
+
96
+ self.context = context
97
+ """::
98
+
99
+ boolean = resource.context
100
+
101
+ Whether the function gets the context of the request.
102
+ """
103
+
104
+ self.description = description
105
+ """::
106
+
107
+ description = resource.description
108
+
109
+ What the resource is, the first paragraph of the docstring of the
110
+ function by default, or ``None``.
111
+ """
112
+
113
+ self.mime_type = mime_type
114
+ """::
115
+
116
+ type = resource.mime_type
117
+
118
+ The media type of the contents, ``text/plain`` by default.
119
+ """
120
+
121
+ self.name = name or cb.__name__
122
+ """::
123
+
124
+ name = resource.name
125
+
126
+ The name of the resource, the one of the function by default.
127
+ """
128
+
129
+ self.title = title
130
+ """::
131
+
132
+ title = resource.title
133
+
134
+ The name of the resource for people, or ``None``.
135
+ """
136
+
137
+ self.uri = uri
138
+ """::
139
+
140
+ uri = resource.uri
141
+
142
+ The URI of the resource, which ``resources/read`` asks for.
143
+ """
144
+
145
+ def __json__(self) -> dict[str, Any]:
146
+ """::
147
+
148
+ definition = resource.__json__()
149
+
150
+ The definition of the resource, as ``resources/list`` gives it, with
151
+ its ``uri``, ``name``, ``title``, ``description`` and ``mimeType``,
152
+ where it has them.
153
+ """
154
+ definition: dict[str, Any] = {'uri': self.uri, 'name': self.name}
155
+ if self.title is not None:
156
+ definition['title'] = self.title
157
+ if self.description is not None:
158
+ definition['description'] = self.description
159
+ definition['mimeType'] = self.mime_type
160
+ return definition
161
+
162
+ def read_p(self, context: Any = None) -> Pyjo.Promise.object:
163
+ """::
164
+
165
+ promise = resource.read_p()
166
+ promise = resource.read_p(context)
167
+
168
+ Call the function, and return a :mod:`Pyjo.Promise` of the result of
169
+ the resource, as ``resources/read`` gives it, without its
170
+ ``resultType``, or rejected with an exception of the function. ::
171
+
172
+ # {'contents': [{'uri': 'file:///project/README.md', 'mimeType': 'text/markdown', 'text': '# Pyjo-MCP'}]}
173
+ resource.read_p().then(print).wait()
174
+ """
175
+ try:
176
+ value = self.cb(*([context] if self.context else []))
177
+ except Exception as e:
178
+ return Pyjo.Promise.reject(e)
179
+ if inspect.iscoroutine(value):
180
+ value = Pyjo.Promise.new(value)
181
+ if isinstance(value, Pyjo.Promise.object):
182
+ return value.then(self.result)
183
+ return Pyjo.Promise.resolve(self.result(value))
184
+
185
+ def result(self, value: Any = None) -> dict[str, Any]:
186
+ """::
187
+
188
+ result = resource.result('# Pyjo-MCP')
189
+ result = resource.result(b'\\x89PNG')
190
+
191
+ The result of the resource of what the function returns.
192
+ """
193
+ if isinstance(value, dict) and 'contents' in value:
194
+ return cast(dict[str, Any], value)
195
+ contents: dict[str, Any] = {'uri': self.uri, 'mimeType': self.mime_type}
196
+ if isinstance(value, bytes | bytearray):
197
+ contents['blob'] = base64.b64encode(value).decode()
198
+ else:
199
+ contents['text'] = value if isinstance(value, str) else Pyjo.JSON.to_json(cast(Any, value))
200
+ return {'contents': [contents]}
201
+
202
+
203
+ new = Pyjo_MCP_Resource.new
204
+ object = Pyjo_MCP_Resource
205
+ Resource = Pyjo_MCP_Resource