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/Tool.py ADDED
@@ -0,0 +1,399 @@
1
+ """
2
+ Pyjo.MCP.Tool - Tool of a server of MCP
3
+ =======================================
4
+ ::
5
+
6
+ from Pyjo.MCP.Tool import Tool
7
+
8
+ def add(a: int, b: int) -> int:
9
+ \"\"\"Add two numbers.
10
+
11
+ :param a: The first number.
12
+ :param b: The second number.
13
+ \"\"\"
14
+ return a + b
15
+
16
+ tool = Tool(add)
17
+ print(tool.name, tool.description, tool.input_schema)
18
+
19
+ # Or import the module, as in Google Python Style Guide
20
+
21
+ import Pyjo.MCP.Tool
22
+
23
+ tool = Pyjo.MCP.Tool.new(add, name='math.add', title='Addition')
24
+ tool.call_p({'a': 1, 'b': 2}).then(print).wait()
25
+
26
+ :mod:`Pyjo.MCP.Tool` is a tool of :mod:`Pyjo.MCP.Server`, a function that a
27
+ language model calls with the arguments of its ``inputSchema``. The name, the
28
+ description and the schemas of the tool are those of the function by default:
29
+ its name, the first paragraph of its docstring, the schema of its parameters,
30
+ with the descriptions of its docstring, and the one of what it returns, of
31
+ :mod:`Pyjo.JSON.Schema`.
32
+
33
+ The function returns the result of the tool, as a value, a :mod:`Pyjo.Promise`
34
+ of it or a coroutine, of an ``async def``:
35
+
36
+ * a value of the ``outputSchema`` of the tool, as of the annotation of what the
37
+ function returns, is the ``structuredContent`` of the result, and its JSON
38
+ the text of its ``content``;
39
+ * a string, without an ``outputSchema``, is the text of the ``content``;
40
+ * a :class:`dict` with ``content``, without an ``outputSchema``, is the result
41
+ as it is, for the content of another type, as an image;
42
+ * ``None`` is a result without content, and another value is its JSON.
43
+
44
+ Arguments that the ``inputSchema`` does not validate, and a function that
45
+ raises :class:`Error`, give a result with ``isError``, whose text the language
46
+ model reads to call the tool again, as a tool reports the errors of its
47
+ execution. Another exception is an error of the server, which the result does
48
+ not tell.
49
+
50
+ See also
51
+ --------
52
+
53
+ :mod:`Pyjo.MCP.Server`, :mod:`Pyjo.JSON.Schema`, :mod:`Pyjo.MCP`.
54
+
55
+ Classes
56
+ -------
57
+ """
58
+
59
+ import Pyjo.Base
60
+ import Pyjo.Exception
61
+ import Pyjo.JSON
62
+ import Pyjo.JSON.Schema
63
+ import Pyjo.Promise
64
+
65
+ import builtins
66
+ import dataclasses
67
+ import datetime
68
+ import enum
69
+ import inspect
70
+ import re
71
+ import uuid
72
+
73
+ from collections.abc import Callable, Mapping
74
+ from typing import Any, cast
75
+
76
+
77
+ type Anything = builtins.object
78
+
79
+ # The characters of a token of a name of a field of HTTP (RFC 9110)
80
+ _TOKEN = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+")
81
+
82
+
83
+ class Error(Pyjo.Exception.object):
84
+ """
85
+ An error of the execution of a tool, which the result of the call tells
86
+ with ``isError``, so that the language model can correct the arguments. ::
87
+
88
+ raise Pyjo.MCP.Tool.Error.new('Invalid departure date: must be in the future')
89
+ """
90
+
91
+
92
+ class Pyjo_MCP_Tool(Pyjo.Base.object):
93
+ """
94
+ :mod:`Pyjo.MCP.Tool` inherits all methods from :mod:`Pyjo.Base` and
95
+ implements the following new ones.
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ cb: Callable[..., Any],
101
+ *,
102
+ name: str | None = None,
103
+ title: str | None = None,
104
+ description: str | None = None,
105
+ input_schema: Mapping[str, Any] | Pyjo.JSON.Schema.object | None = None,
106
+ output_schema: Mapping[str, Any] | Pyjo.JSON.Schema.object | bool | None = None,
107
+ annotations: Mapping[str, Any] | None = None,
108
+ context: bool = False,
109
+ ) -> None:
110
+ """::
111
+
112
+ tool = Pyjo.MCP.Tool.new(add)
113
+ tool = Pyjo.MCP.Tool.new(add, name='math.add', title='Addition')
114
+ tool = Pyjo.MCP.Tool.new(lambda c, **args: ..., input_schema={'type': 'object'}, context=True)
115
+ tool = Pyjo.MCP.Tool.new(report, output_schema=False)
116
+
117
+ Construct a new :mod:`Pyjo.MCP.Tool` object of a function. With
118
+ ``context``, the function gets the :class:`~Pyjo.MCP.Server.Context` of
119
+ the request before its arguments. ``output_schema`` of ``False`` gives
120
+ the tool no ``outputSchema``, also when the function has the annotation
121
+ of what it returns.
122
+ """
123
+ signature = inspect.signature(cb)
124
+ skip = 1 if context else 0
125
+
126
+ if input_schema is None:
127
+ schema = Pyjo.JSON.Schema.parameters(cb, skip=skip)
128
+ data = cast(dict[str, Any], schema.data)
129
+ if description is None:
130
+ description = data.pop('description', None)
131
+ else:
132
+ data.pop('description', None)
133
+ elif isinstance(input_schema, Pyjo.JSON.Schema.object):
134
+ schema = input_schema
135
+ else:
136
+ schema = Pyjo.JSON.Schema.new(dict(input_schema))
137
+
138
+ output: Pyjo.JSON.Schema.object | None = None
139
+ if output_schema is None or output_schema is True:
140
+ returns = Pyjo.JSON.Schema.returns(cb)
141
+ # A string is the text of the content, and None no content
142
+ if returns is not None and cast(dict[str, Any], returns.data).get('type') not in ('string', 'null'):
143
+ output = returns
144
+ elif isinstance(output_schema, Pyjo.JSON.Schema.object):
145
+ output = output_schema
146
+ elif output_schema is not False:
147
+ output = Pyjo.JSON.Schema.new(dict(output_schema))
148
+
149
+ self.annotations: dict[str, Any] = dict(annotations or {})
150
+ """::
151
+
152
+ annotations = tool.annotations
153
+
154
+ The properties of the behaviour of the tool, as ``readOnlyHint``, which
155
+ a client takes as hints, for the ``annotations`` of the tool.
156
+ """
157
+
158
+ self.cb = cb
159
+ """::
160
+
161
+ cb = tool.cb
162
+
163
+ The function of the tool.
164
+ """
165
+
166
+ self.context = context
167
+ """::
168
+
169
+ boolean = tool.context
170
+
171
+ Whether the function gets the context of the request before its
172
+ arguments.
173
+ """
174
+
175
+ self.description = description or ''
176
+ """::
177
+
178
+ description = tool.description
179
+
180
+ What the tool does, for the language model, the first paragraph of the
181
+ docstring of the function by default.
182
+ """
183
+
184
+ self.input_schema: Pyjo.JSON.Schema.object = schema
185
+ """::
186
+
187
+ schema = tool.input_schema
188
+
189
+ The :mod:`Pyjo.JSON.Schema` of the arguments, the ``inputSchema`` of
190
+ the tool, the one of the parameters of the function by default.
191
+ """
192
+
193
+ self.name = name or cb.__name__
194
+ """::
195
+
196
+ name = tool.name
197
+
198
+ The name of the tool, the one of the function by default.
199
+ """
200
+
201
+ self.output_schema = output
202
+ """::
203
+
204
+ schema = tool.output_schema
205
+
206
+ The :mod:`Pyjo.JSON.Schema` of the ``structuredContent`` of the result,
207
+ the ``outputSchema`` of the tool, the one of what the function returns
208
+ by default, or ``None``.
209
+ """
210
+
211
+ self.title = title
212
+ """::
213
+
214
+ title = tool.title
215
+
216
+ The name of the tool for people, or ``None``.
217
+ """
218
+
219
+ self._header_params = _header_params_of(schema.data, self.name)
220
+ self._signature = signature
221
+
222
+ def __json__(self) -> dict[str, Any]:
223
+ """::
224
+
225
+ definition = tool.__json__()
226
+
227
+ The definition of the tool, as ``tools/list`` gives it, with its
228
+ ``name``, ``title``, ``description``, ``inputSchema``,
229
+ ``outputSchema`` and ``annotations``, where it has them.
230
+ """
231
+ definition: dict[str, Any] = {'name': self.name}
232
+ if self.title is not None:
233
+ definition['title'] = self.title
234
+ definition['description'] = self.description
235
+ definition['inputSchema'] = self.input_schema.data
236
+ if self.output_schema is not None:
237
+ definition['outputSchema'] = self.output_schema.data
238
+ if self.annotations:
239
+ definition['annotations'] = self.annotations
240
+ return definition
241
+
242
+ def call_p(self, arguments: Mapping[str, Any], context: Any = None) -> Pyjo.Promise.object:
243
+ """::
244
+
245
+ promise = tool.call_p({'a': 1, 'b': 2})
246
+ promise = tool.call_p({'a': 1, 'b': 2}, context)
247
+
248
+ Call the function with the arguments, and return a :mod:`Pyjo.Promise`
249
+ of the result of the tool, as ``tools/call`` gives it, without its
250
+ ``resultType``, or rejected with an exception of the function that is not an
251
+ :class:`Error`, an error of the server. ::
252
+
253
+ # {'content': [{'type': 'text', 'text': '3'}], 'structuredContent': 3, 'isError': False}
254
+ tool.call_p({'a': 1, 'b': 2}).then(print).wait()
255
+ """
256
+ if not (result := self.input_schema.validate(dict(arguments))):
257
+ return Pyjo.Promise.resolve(error_result(f'Invalid arguments:\n{result}'))
258
+ try:
259
+ bound = self._signature.bind(*([context] if self.context else []), **arguments)
260
+ except TypeError as e:
261
+ return Pyjo.Promise.resolve(error_result(f'Invalid arguments: {e}'))
262
+
263
+ try:
264
+ value = self.cb(*bound.args, **bound.kwargs)
265
+ except Error as e:
266
+ return Pyjo.Promise.resolve(error_result(e.message))
267
+ except Exception as e:
268
+ return Pyjo.Promise.reject(e)
269
+ if inspect.iscoroutine(value):
270
+ value = Pyjo.Promise.new(value)
271
+ if isinstance(value, Pyjo.Promise.object):
272
+
273
+ def failed(e: Any = None) -> Any:
274
+ if isinstance(e, Error):
275
+ return error_result(e.message)
276
+ return Pyjo.Promise.reject(e)
277
+
278
+ return value.then(self.result, failed)
279
+ try:
280
+ return Pyjo.Promise.resolve(self.result(value))
281
+ except Exception as e:
282
+ return Pyjo.Promise.reject(e)
283
+
284
+ @property
285
+ def header_params(self) -> list[tuple[str, list[str], str]]:
286
+ """::
287
+
288
+ params = tool.header_params
289
+
290
+ The arguments of the ``x-mcp-header`` of the ``inputSchema``, which a
291
+ client of HTTP sends as the headers ``Mcp-Param-{name}`` too, each with
292
+ the name, the path of the properties and the type. A header that the
293
+ specification does not allow, as of a property of an array or of a
294
+ ``number``, fails with a :mod:`Pyjo.Exception` when the tool is made.
295
+ """
296
+ return self._header_params
297
+
298
+ def result(self, value: Any = None) -> dict[str, Any]:
299
+ """::
300
+
301
+ result = tool.result(3)
302
+ result = tool.result('Three')
303
+
304
+ The result of the tool of what the function returns. A value that the
305
+ ``outputSchema`` does not validate fails with a :mod:`Pyjo.Exception`,
306
+ as an error of the server.
307
+ """
308
+ if self.output_schema is not None:
309
+ data = _plain(value)
310
+ if not (result := self.output_schema.validate(data)):
311
+ raise Pyjo.Exception.new(f'The result of the tool {self.name} is not valid:\n{result}')
312
+ text = Pyjo.JSON.to_json(data)
313
+ return {'content': [{'type': 'text', 'text': text}], 'structuredContent': data, 'isError': False}
314
+ if value is None:
315
+ return {'content': [], 'isError': False}
316
+ if isinstance(value, dict) and 'content' in value:
317
+ return cast(dict[str, Any], value)
318
+ data = _plain(value)
319
+ return text_result(data if isinstance(data, str) else Pyjo.JSON.to_json(data))
320
+
321
+
322
+ def error_result(text: str) -> dict[str, Any]:
323
+ """::
324
+
325
+ result = Pyjo.MCP.Tool.error_result('Invalid departure date')
326
+
327
+ A result of a tool with the text of an error of its execution and
328
+ ``isError``.
329
+ """
330
+ return {'content': [{'type': 'text', 'text': text}], 'isError': True}
331
+
332
+
333
+ def text_result(text: str) -> dict[str, Any]:
334
+ """::
335
+
336
+ result = Pyjo.MCP.Tool.text_result('Sunny')
337
+
338
+ A result of a tool with a text.
339
+ """
340
+ return {'content': [{'type': 'text', 'text': text}], 'isError': False}
341
+
342
+
343
+ def _header_params_of(schema: Anything, tool: str) -> list[tuple[str, list[str], str]]:
344
+ params: list[tuple[str, list[str], str]] = []
345
+ _header_params(schema, [], params, False)
346
+ names = [name.lower() for name, _, _ in params]
347
+ if len(set(names)) != len(names):
348
+ raise Pyjo.Exception.new(f'The x-mcp-header of the tool {tool} are not unique')
349
+ return params
350
+
351
+
352
+ def _header_params(schema: Anything, path: list[str], params: list[tuple[str, list[str], str]], hidden: bool) -> None:
353
+ # The properties that have an x-mcp-header, through the chain of properties from the root only
354
+ if not isinstance(schema, dict):
355
+ return
356
+ schema_ = cast(dict[str, Any], schema)
357
+ for keyword, value in schema_.items():
358
+ if keyword == 'properties' and isinstance(value, dict):
359
+ for key, property in cast(dict[str, Any], value).items():
360
+ _header_params(property, [*path, key], params, hidden)
361
+ elif keyword in ('items', 'prefixItems', 'additionalProperties', 'patternProperties', 'contains', '$defs'):
362
+ _header_params(value, path, params, True)
363
+ elif keyword in ('allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else', 'dependentSchemas'):
364
+ for each in cast(list[Any], value) if isinstance(value, list) else [value]:
365
+ _header_params(each, path, params, True)
366
+ if (name := schema_.get('x-mcp-header')) is None:
367
+ return
368
+ type = schema_.get('type')
369
+ if hidden or not path:
370
+ raise Pyjo.Exception.new(f'The x-mcp-header {name} is not of a property of the root')
371
+ if not isinstance(name, str) or not _TOKEN.fullmatch(name):
372
+ raise Pyjo.Exception.new(f'The x-mcp-header {name!r} is not a name of a header')
373
+ if type not in ('integer', 'string', 'boolean'):
374
+ raise Pyjo.Exception.new(f'The x-mcp-header {name} is not of an integer, a string or a boolean')
375
+ params.append((name, path, type))
376
+
377
+
378
+ def _plain(value: Any) -> Any:
379
+ # The value as JSON has it, of the types that Pyjo.JSON.Schema gives a schema
380
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
381
+ return {field.name: _plain(getattr(value, field.name)) for field in dataclasses.fields(value)}
382
+ if isinstance(value, enum.Enum):
383
+ return _plain(value.value)
384
+ if isinstance(value, datetime.datetime | datetime.date | datetime.time):
385
+ return value.isoformat()
386
+ if isinstance(value, uuid.UUID):
387
+ return str(value)
388
+ if isinstance(value, dict):
389
+ return {str(key): _plain(item) for key, item in cast(dict[Any, Any], value).items()}
390
+ if isinstance(value, list | tuple | set | frozenset):
391
+ return [_plain(item) for item in cast(list[Any], value)]
392
+ if value is None or isinstance(value, bool | int | float | str):
393
+ return value
394
+ return Pyjo.JSON.decode_json(Pyjo.JSON.encode_json(value))
395
+
396
+
397
+ new = Pyjo_MCP_Tool.new
398
+ object = Pyjo_MCP_Tool
399
+ Tool = Pyjo_MCP_Tool
Pyjo/MCP/__init__.py ADDED
@@ -0,0 +1,80 @@
1
+ """
2
+ Pyjo.MCP - Model Context Protocol for Pyjoyment
3
+ ===============================================
4
+ ::
5
+
6
+ import Pyjo.MCP
7
+
8
+ print(Pyjo.MCP.PROTOCOL_VERSION)
9
+
10
+ # Pyjoyment.Lite
11
+ from Pyjo.MCP.Server import Server
12
+ from Pyjoyment.Lite import *
13
+
14
+ server = Server(name='calculator', version='1.0.0')
15
+
16
+ @server.tool
17
+ def add(a: int, b: int) -> int:
18
+ \"\"\"Add two numbers.\"\"\"
19
+ return a + b
20
+
21
+ plugin('AllowedHosts')
22
+ any('/mcp', server.to_action())
23
+
24
+ app.start()
25
+
26
+ :mod:`Pyjo.MCP` is the namespace of the `Model Context Protocol
27
+ <https://modelcontextprotocol.io>`_ for :mod:`Pyjoyment`, a server and a client
28
+ made with Pyjoyment, of the revision :data:`PROTOCOL_VERSION` of the
29
+ specification, whose core is stateless: every request carries its version and
30
+ the capabilities of its client in its ``_meta``, without a session.
31
+
32
+ :mod:`Pyjo.MCP.Server` is the server, whose tools, prompts, resources and
33
+ resource templates are :mod:`Pyjo.MCP.Tool`, :mod:`Pyjo.MCP.Prompt`,
34
+ :mod:`Pyjo.MCP.Resource` and :mod:`Pyjo.MCP.ResourceTemplate` objects,
35
+ :mod:`Pyjo.MCP.Client` is the client, :mod:`Pyjo.MCP.Test` tests a server
36
+ with its requests, and the messages are those of :mod:`Pyjo.JSON.RPC`.
37
+
38
+ See also
39
+ --------
40
+
41
+ :mod:`Pyjo.MCP.Server`, :mod:`Pyjo.MCP.Tool`, :mod:`Pyjo.MCP.Prompt`,
42
+ :mod:`Pyjo.MCP.Resource`, :mod:`Pyjo.MCP.ResourceTemplate`,
43
+ :mod:`Pyjo.MCP.Client`, :mod:`Pyjo.MCP.Test`, :mod:`Pyjoyment`,
44
+ https://github.com/dex4er/Pyjoyment.
45
+
46
+ It is inspired by the distribution `MCP <https://metacpan.org/pod/MCP>`_ of
47
+ CPAN, built on Mojolicious.
48
+ """
49
+
50
+ import importlib.metadata
51
+
52
+
53
+ __version__ = importlib.metadata.version('Pyjo-MCP')
54
+ """The version of the distribution, of its metadata, which the workflow Automated version sets in pyproject.toml."""
55
+
56
+ PROTOCOL_VERSION = '2026-07-28'
57
+ """The revision of the specification of the Model Context Protocol, as ``MCP-Protocol-Version`` names it."""
58
+
59
+ SUPPORTED_VERSIONS = (PROTOCOL_VERSION,)
60
+ """The revisions that the server supports, as ``server/discover`` gives them."""
61
+
62
+ HEADER_MISMATCH = -32020
63
+ """The code of the error of a header of HTTP that does not match the body of the request, or that is missing."""
64
+ MISSING_REQUIRED_CLIENT_CAPABILITY = -32021
65
+ """The code of the error of a request that needs a capability that its client does not declare."""
66
+ UNSUPPORTED_PROTOCOL_VERSION = -32022
67
+ """The code of the error of a request of a revision that the server does not support."""
68
+
69
+ META_CLIENT_CAPABILITIES = 'io.modelcontextprotocol/clientCapabilities'
70
+ """The key of ``_meta`` of a request with the capabilities of its client."""
71
+ META_CLIENT_INFO = 'io.modelcontextprotocol/clientInfo'
72
+ """The key of ``_meta`` of a request with the name and the version of its client."""
73
+ META_LOG_LEVEL = 'io.modelcontextprotocol/logLevel'
74
+ """The key of ``_meta`` of a request with the level of the log messages that its client wants."""
75
+ META_PROTOCOL_VERSION = 'io.modelcontextprotocol/protocolVersion'
76
+ """The key of ``_meta`` of a request with its revision of the specification."""
77
+ META_SERVER_INFO = 'io.modelcontextprotocol/serverInfo'
78
+ """The key of ``_meta`` of a result with the name and the version of its server."""
79
+ META_SUBSCRIPTION_ID = 'io.modelcontextprotocol/subscriptionId'
80
+ """The key of ``_meta`` of a message of a subscription with the ``id`` of its request ``subscriptions/listen``."""
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.5
2
+ Name: Pyjo-MCP
3
+ Version: 0.1.0
4
+ Summary: Model Context Protocol server and client for Pyjoyment.
5
+ Project-URL: Homepage, https://github.com/dex4er/Pyjo-MCP
6
+ Project-URL: Documentation, https://pyjoyment.readthedocs.io/projects/pyjo-mcp/en/latest/
7
+ Author-email: Piotr Roszatycki <piotr.roszatycki@gmail.com>
8
+ License-Expression: Artistic-2.0
9
+ License-File: LICENSE
10
+ Keywords: mcp,model-context-protocol,pyjoyment
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Requires-Python: >=3.12
15
+ Requires-Dist: pyjoyment>=0.1.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ [![Home](https://img.shields.io/badge/%F0%9F%8F%A0-Pyjoyment-02a7a0)](https://github.com/dex4er/Pyjoyment)
19
+ [![GitHub](https://img.shields.io/github/stars/dex4er/Pyjo-MCP?style=flat&label=GitHub%20%E2%AD%90&logo=github)](https://github.com/dex4er/Pyjo-MCP)
20
+ [![Documentation](https://img.shields.io/badge/docs-Read_the_Docs-blue?logo=readthedocs&logoColor=white)](https://pyjoyment.readthedocs.io/projects/pyjo-mcp/en/latest/)
21
+ [![License](https://img.shields.io/github/license/dex4er/Pyjo-MCP.svg)](https://github.com/dex4er/Pyjo-MCP/blob/main/LICENSE)
22
+ [![Tests](https://github.com/dex4er/Pyjo-MCP/actions/workflows/tests.yaml/badge.svg?branch=main)](https://github.com/dex4er/Pyjo-MCP/actions/workflows/tests.yaml)
23
+
24
+ # Pyjo-MCP
25
+
26
+ <p align="center">
27
+ <picture>
28
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/dex4er/Pyjoyment/main/Pyjoyment/resources/public/pyjo/logo-white-4x.png">
29
+ <img src="https://raw.githubusercontent.com/dex4er/Pyjoyment/main/Pyjoyment/resources/public/pyjo/logo-black-4x.png" alt="Pyjoyment" width="322">
30
+ </picture>
31
+ </p>
32
+
33
+ [Model Context Protocol](https://modelcontextprotocol.io) server and client
34
+ for [Pyjoyment](https://github.com/dex4er/Pyjoyment), of the revision
35
+ 2026-07-28 of the specification.
36
+
37
+ ## Installation
38
+
39
+ From [PyPI](https://pypi.org/project/Pyjo-MCP/), with
40
+ [Pyjoyment](https://pypi.org/project/Pyjoyment/), which it depends on:
41
+
42
+ ```sh
43
+ pip install Pyjo-MCP
44
+ ```
45
+
46
+ or, in a project managed with [uv](https://docs.astral.sh/uv/):
47
+
48
+ ```sh
49
+ uv add Pyjo-MCP
50
+ ```
51
+
52
+ The development version is installed from the Git repository:
53
+
54
+ ```sh
55
+ pip install git+https://github.com/dex4er/Pyjo-MCP
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ A server of MCP is a route of an application of Pyjoyment, whose tools are
61
+ functions, with the schemas of their type hints and the descriptions of their
62
+ docstrings:
63
+
64
+ ```python
65
+ from Pyjo.MCP.Server import Server
66
+ from Pyjo.MCP.Tool import Error
67
+ from Pyjoyment.Lite import *
68
+
69
+ server = Server(name='calculator', version='1.0.0')
70
+
71
+
72
+ @server.tool
73
+ def divide(a: float, b: float) -> float:
74
+ """Divide the first number by the second one."""
75
+ if b == 0:
76
+ raise Error.new('Division by zero')
77
+ return a / b
78
+
79
+
80
+ # Host and Origin of localhost only, against DNS rebinding
81
+ plugin('AllowedHosts')
82
+
83
+ any('/mcp', server.to_action())
84
+
85
+ app.start()
86
+ ```
87
+
88
+ ```sh
89
+ uv run python calculator.py daemon -l http://127.0.0.1:3000
90
+ ```
91
+
92
+ Its endpoint is `http://127.0.0.1:3000/mcp`, of the transport Streamable HTTP.
93
+ The whole calculator is [`examples/calculator.py`](examples/calculator.py).
94
+
95
+ Prompts, the templates of messages that a user picks, and resources, the data
96
+ that a client reads by its URI, are functions too:
97
+
98
+ ```python
99
+ @server.prompt
100
+ def explain(expression: str) -> str:
101
+ """Explain an arithmetic expression step by step."""
102
+ return f'Explain step by step how to compute {expression}.'
103
+
104
+
105
+ @server.resource('calculator://precision', mime_type='application/json')
106
+ def precision() -> dict:
107
+ """The precision of the numbers of the calculator."""
108
+ return {'digits': 15}
109
+ ```
110
+
111
+ The same application serves the transport stdio too, with a hook:
112
+
113
+ ```python
114
+ hook('before_server_start', server.to_stdio())
115
+ ```
116
+
117
+ A client of MCP, as Claude Code, then starts it with the command `stdio`:
118
+
119
+ ```sh
120
+ claude mcp add calculator -- uv run python examples/calculator.py stdio
121
+ ```
122
+
123
+ [`examples/weather.py`](examples/weather.py) is a gateway to the weather of
124
+ [Open-Meteo](https://open-meteo.com), which needs no key, with the tools
125
+ `find_places`, `current_weather`, `forecast` and `hourly_forecast`, which
126
+ request the API with the non-blocking user agent of the application. A client of MCP, as Claude Code,
127
+ connects to it over HTTP:
128
+
129
+ ```sh
130
+ uv run python examples/weather.py daemon -l http://127.0.0.1:3000
131
+ claude mcp add --transport http weather http://127.0.0.1:3000/mcp
132
+ ```
133
+
134
+ [`examples/mcp_cli.py`](examples/mcp_cli.py) is a client of the command line
135
+ for the tools of any server of MCP: a tool is a command, in kebab-case, and its
136
+ arguments are options, with the descriptions and the constraints of its
137
+ `inputSchema` in the help of the command. The result is text, with tables of
138
+ the lists of objects, or JSON:
139
+
140
+ ```sh
141
+ uv run python examples/mcp_cli.py --help
142
+ uv run python examples/mcp_cli.py forecast --place Kraków --language pl --days 2
143
+ uv run python examples/mcp_cli.py hourly-forecast -p Warsaw -H 6 -f json
144
+ ```
145
+
146
+ It is made with `Pyjo.MCP.Client`, a client of MCP over Streamable HTTP, which
147
+ sends the `_meta` and the headers that every request carries:
148
+
149
+ ```python
150
+ from Pyjo.MCP.Client import Client
151
+
152
+ client = Client('http://127.0.0.1:3000/mcp')
153
+ print(client.call_tool('divide', {'a': 7, 'b': 2})['structuredContent'])
154
+ ```
155
+
156
+ `Pyjo.MCP.Test` tests a server with the requests of the client, without
157
+ starting it:
158
+
159
+ ```python
160
+ import Pyjo.MCP.Test
161
+
162
+ from Pyjo.File import curfile
163
+ from Pyjo.Test import *
164
+
165
+ t = Pyjo.MCP.Test.new(curfile().sibling('calculator.py'))
166
+ t.call_ok('divide', a=7, b=2).status_is(200).structured_content_is(3.5)
167
+ t.call_ok('divide', a=1, b=0).is_error_is(True).text_content_is('Division by zero')
168
+
169
+ done_testing()
170
+ ```
171
+
172
+ ## Documentation
173
+
174
+ The tutorial, which makes servers of MCP step by step, and the API of the package
175
+ are on [Read the Docs](https://pyjoyment.readthedocs.io/projects/pyjo-mcp/en/latest/).
176
+
177
+ ## Status
178
+
179
+ A server of the tools, the prompts and the resources of MCP over Streamable
180
+ HTTP and stdio, with `server/discover`, `tools/list`, `tools/call`,
181
+ `prompts/list`, `prompts/get`, `resources/list`, `resources/templates/list`,
182
+ `resources/read`, `subscriptions/listen` and `completion/complete`, a client
183
+ of them over Streamable HTTP, and assertions of tests with its requests. A tool
184
+ sends its progress and its log messages before its result, and a subscription
185
+ gets the changes of the lists and of the resources. A resource template is a
186
+ URI Template of RFC 6570, whose URIs a client reads. A tool, a prompt and a
187
+ resource ask the user, the language model or the roots of the client for
188
+ input, in the rounds of a multi round-trip request, and the arguments of the
189
+ prompts and the variables of the templates complete their values.
190
+
191
+ ## License
192
+
193
+ The Artistic License 2.0, as Pyjoyment.
@@ -0,0 +1,12 @@
1
+ Pyjo/MCP/Client.py,sha256=KJd-rQLzS7RBZymxsXnUd_YaE_oi80-sYAVBP9bdiFU,33292
2
+ Pyjo/MCP/Prompt.py,sha256=KsP46nlimkBYvYa5miniyvtC5EGee5lbPziwwVCtuwg,9338
3
+ Pyjo/MCP/Resource.py,sha256=RgA7t2qBemMqlfR4j4-Ok7NmfZfRQKPlCsmsdu2Q8Jc,6202
4
+ Pyjo/MCP/ResourceTemplate.py,sha256=DtoMGx7HwTOMVjFZtWtye2hc_mfXLgjQLBvRcQkkWbA,9247
5
+ Pyjo/MCP/Server.py,sha256=9f_GeEA8Q6T8NsOhXU-Qhyxm3IDsWhYXiuhFAeCnGpM,63892
6
+ Pyjo/MCP/Test.py,sha256=n9jcYICIVdoV2mPVcbaOf6Vpgdzbqw6hBVAQBdePFq0,31612
7
+ Pyjo/MCP/Tool.py,sha256=WlzTWm7iOBMMODzFJcnhZORoIkAVjSJbVDfpZEHsdE0,14689
8
+ Pyjo/MCP/__init__.py,sha256=zXw7rQlO8uYhgDxeZ-rVCIlnlFNG7G-jA9EX8FxuPaE,3365
9
+ pyjo_mcp-0.1.0.dist-info/METADATA,sha256=wsTqnrngC77eL7Hkd_v6rlw2s27k0x4_pEPOZ2DCAbQ,6843
10
+ pyjo_mcp-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
11
+ pyjo_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=wqf_RI5i8Of0e_6xwFe7wBhw72jXAkF_9N8Ny9ScaP4,8894
12
+ pyjo_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any