py2mcp 0.1.1__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.
py2mcp/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """py2mcp: Quick MCP server creation from Python functions.
2
+
3
+ This package provides a simple, Pythonic way to create Model Context Protocol (MCP)
4
+ servers from ordinary Python functions. Built on FastMCP, it handles all the protocol
5
+ complexity while letting you focus on your business logic.
6
+
7
+ Basic usage:
8
+ >>> from py2mcp import mk_mcp_server
9
+ >>>
10
+ >>> def add(a: int, b: int) -> int:
11
+ ... '''Add two numbers'''
12
+ ... return a + b
13
+ >>>
14
+ >>> mcp = mk_mcp_server([add])
15
+ >>> # mcp.run() # Start the server
16
+ """
17
+
18
+ from py2mcp.main import mk_mcp_server, mk_mcp_from_store
19
+ from py2mcp.trans import mk_input_trans
20
+
21
+ __version__ = "0.1.0"
22
+
23
+ __all__ = [
24
+ "mk_mcp_server",
25
+ "mk_mcp_from_store",
26
+ "mk_input_trans",
27
+ ]
py2mcp/base.py ADDED
@@ -0,0 +1,49 @@
1
+ """Base objects and utilities for py2mcp."""
2
+
3
+ from typing import Callable, Iterable, Any, Optional
4
+ from functools import wraps
5
+
6
+
7
+ def _wrap_with_input_trans(func: Callable, input_trans: Optional[Callable]) -> Callable:
8
+ """Wrap a function to apply input transformation.
9
+
10
+ >>> def double(x): return x * 2
11
+ >>> def add_one_trans(kwargs): return {k: v + 1 for k, v in kwargs.items()}
12
+ >>> wrapped = _wrap_with_input_trans(double, add_one_trans)
13
+ >>> wrapped(x=5)
14
+ 12
15
+ """
16
+ if input_trans is None:
17
+ return func
18
+
19
+ @wraps(func)
20
+ def wrapper(**kwargs):
21
+ transformed = input_trans(kwargs)
22
+ return func(**transformed)
23
+
24
+ # Preserve original function metadata for introspection
25
+ wrapper.__wrapped__ = func
26
+ return wrapper
27
+
28
+
29
+ def _normalize_to_iterable(funcs: Any) -> Iterable[Callable]:
30
+ """Normalize input to an iterable of callables.
31
+
32
+ >>> def f(): pass
33
+ >>> def g(): pass
34
+ >>> list(_normalize_to_iterable(f))
35
+ [<function f at ...>]
36
+ >>> len(list(_normalize_to_iterable([f, g])))
37
+ 2
38
+ """
39
+ if callable(funcs):
40
+ return [funcs]
41
+ elif isinstance(funcs, Iterable):
42
+ result = list(funcs)
43
+ if not all(callable(f) for f in result):
44
+ raise TypeError("All items must be callable")
45
+ return result
46
+ else:
47
+ raise TypeError(
48
+ f"Expected callable or iterable of callables, got {type(funcs)}"
49
+ )
py2mcp/main.py ADDED
@@ -0,0 +1,91 @@
1
+ """Main entry point for creating MCP servers from Python functions."""
2
+
3
+ from typing import Callable, Iterable, Optional, MutableMapping, Any
4
+ from fastmcp import FastMCP
5
+
6
+ from py2mcp.base import _normalize_to_iterable, _wrap_with_input_trans
7
+ from py2mcp.util import store_to_funcs
8
+
9
+
10
+ def mk_mcp_server(
11
+ funcs: Callable | Iterable[Callable],
12
+ *,
13
+ name: str = "py2mcp Server",
14
+ input_trans: Optional[Callable[[dict], dict]] = None,
15
+ ) -> FastMCP:
16
+ """Create an MCP server from Python functions.
17
+
18
+ This is the main entry point for py2mcp. Pass one or more functions,
19
+ and get back a FastMCP server ready to run.
20
+
21
+ Args:
22
+ funcs: A function or iterable of functions to expose as MCP tools
23
+ name: Name of the MCP server
24
+ input_trans: Optional function to transform input kwargs before calling tools
25
+
26
+ Returns:
27
+ A FastMCP server instance ready to run
28
+
29
+ Examples:
30
+ >>> def add(a: int, b: int) -> int:
31
+ ... '''Add two numbers'''
32
+ ... return a + b
33
+ >>> mcp = mk_mcp_server(add)
34
+ >>> mcp.name
35
+ 'py2mcp Server'
36
+
37
+ >>> def greet(name: str) -> str:
38
+ ... return f"Hello, {name}!"
39
+ >>> mcp = mk_mcp_server([add, greet], name="Math & Greetings")
40
+ >>> mcp.name
41
+ 'Math & Greetings'
42
+ """
43
+ mcp = FastMCP(name)
44
+
45
+ # Normalize to list of functions
46
+ func_list = list(_normalize_to_iterable(funcs))
47
+
48
+ # Register each function as a tool
49
+ for func in func_list:
50
+ # Wrap with input transformation if provided
51
+ if input_trans is not None:
52
+ func = _wrap_with_input_trans(func, input_trans)
53
+
54
+ # Register as MCP tool
55
+ mcp.tool(func)
56
+
57
+ return mcp
58
+
59
+
60
+ def mk_mcp_from_store(
61
+ store: MutableMapping[Any, Any],
62
+ *,
63
+ name: str = "item",
64
+ plural: str = "",
65
+ server_name: Optional[str] = None,
66
+ ) -> FastMCP:
67
+ """Create an MCP server from a MutableMapping with CRUD operations.
68
+
69
+ Automatically generates list, get, set, and delete functions for the store.
70
+
71
+ Args:
72
+ store: A MutableMapping to expose via MCP
73
+ name: Singular name for items (e.g., 'project', 'user')
74
+ plural: Plural form (defaults to name + 's')
75
+ server_name: Name of the MCP server (defaults to "{name} Store")
76
+
77
+ Returns:
78
+ A FastMCP server with CRUD operations
79
+
80
+ Examples:
81
+ >>> projects = {'p1': {'name': 'Project 1'}, 'p2': {'name': 'Project 2'}}
82
+ >>> mcp = mk_mcp_from_store(projects, name='project')
83
+ >>> mcp.name
84
+ 'project Store'
85
+ """
86
+ if server_name is None:
87
+ server_name = f"{name} Store"
88
+
89
+ funcs = store_to_funcs(store, name=name, plural=plural)
90
+
91
+ return mk_mcp_server(funcs, name=server_name)
@@ -0,0 +1,241 @@
1
+ """Tests for py2mcp functionality."""
2
+
3
+ import pytest
4
+ import asyncio
5
+
6
+ from py2mcp import mk_mcp_server, mk_mcp_from_store, mk_input_trans
7
+
8
+
9
+ # ---------------------------------------------------------------------------
10
+ # Helpers
11
+ # ---------------------------------------------------------------------------
12
+
13
+ def _run(coro):
14
+ """Run a coroutine synchronously."""
15
+ return asyncio.run(coro)
16
+
17
+
18
+ def _call(mcp, tool_name, args=None):
19
+ """Call a tool on an MCP server and return the structured result."""
20
+ result = _run(mcp.call_tool(tool_name, args or {}))
21
+ return result.structured_content.get('result', result.structured_content)
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Unit tests
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ def test_single_function():
30
+ """Test creating an MCP server from a single function."""
31
+ def add(a: int, b: int) -> int:
32
+ return a + b
33
+
34
+ mcp = mk_mcp_server(add)
35
+ assert mcp is not None
36
+ assert mcp.name == "py2mcp Server"
37
+
38
+
39
+ def test_multiple_functions():
40
+ """Test creating an MCP server from multiple functions."""
41
+ def add(a: int, b: int) -> int:
42
+ return a + b
43
+
44
+ def multiply(a: int, b: int) -> int:
45
+ return a * b
46
+
47
+ mcp = mk_mcp_server([add, multiply], name="Math Server")
48
+ assert mcp.name == "Math Server"
49
+
50
+
51
+ def test_input_transformation():
52
+ """Test input transformation."""
53
+ trans = mk_input_trans({'x': int, 'y': float})
54
+
55
+ result = trans({'x': '42', 'y': '3.14', 'z': 'unchanged'})
56
+ assert result['x'] == 42
57
+ assert result['y'] == 3.14
58
+ assert result['z'] == 'unchanged'
59
+
60
+
61
+ def test_input_trans_with_none():
62
+ """Test that None input_trans works correctly."""
63
+ trans = mk_input_trans(None)
64
+
65
+ result = trans({'a': 1, 'b': 2})
66
+ assert result == {'a': 1, 'b': 2}
67
+
68
+
69
+ def test_store_to_mcp():
70
+ """Test creating MCP server from a store."""
71
+ store = {'item1': 'value1', 'item2': 'value2'}
72
+
73
+ mcp = mk_mcp_from_store(store, name='item')
74
+ assert mcp.name == 'item Store'
75
+
76
+
77
+ def test_store_operations():
78
+ """Test that store operations work correctly."""
79
+ from py2mcp.util import store_to_funcs
80
+
81
+ store = {'a': 1, 'b': 2}
82
+ funcs = store_to_funcs(store, name='item')
83
+
84
+ assert len(funcs) == 4
85
+
86
+ func_names = [f.__name__ for f in funcs]
87
+ assert 'list_items' in func_names
88
+ assert 'get_item' in func_names
89
+ assert 'set_item' in func_names
90
+ assert 'delete_item' in func_names
91
+
92
+
93
+ def test_store_list_operation():
94
+ """Test list operation on store."""
95
+ from py2mcp.util import store_to_funcs
96
+
97
+ store = {'a': 1, 'b': 2, 'c': 3}
98
+ funcs = {f.__name__: f for f in store_to_funcs(store, name='item')}
99
+
100
+ items = funcs['list_items']()
101
+ assert set(items) == {'a', 'b', 'c'}
102
+
103
+
104
+ def test_store_get_operation():
105
+ """Test get operation on store."""
106
+ from py2mcp.util import store_to_funcs
107
+
108
+ store = {'x': 100, 'y': 200}
109
+ funcs = {f.__name__: f for f in store_to_funcs(store, name='item')}
110
+
111
+ assert funcs['get_item']('x') == 100
112
+ assert funcs['get_item']('y') == 200
113
+
114
+
115
+ def test_store_set_operation():
116
+ """Test set operation on store."""
117
+ from py2mcp.util import store_to_funcs
118
+
119
+ store = {}
120
+ funcs = {f.__name__: f for f in store_to_funcs(store, name='item')}
121
+
122
+ funcs['set_item']('new_key', 'new_value')
123
+ assert store['new_key'] == 'new_value'
124
+
125
+
126
+ def test_store_delete_operation():
127
+ """Test delete operation on store."""
128
+ from py2mcp.util import store_to_funcs
129
+
130
+ store = {'to_delete': 'value'}
131
+ funcs = {f.__name__: f for f in store_to_funcs(store, name='item')}
132
+
133
+ funcs['delete_item']('to_delete')
134
+ assert 'to_delete' not in store
135
+
136
+
137
+ def test_store_custom_plural():
138
+ """Test store functions with custom plural form."""
139
+ from py2mcp.util import store_to_funcs
140
+
141
+ store = {'a': 1}
142
+ funcs = store_to_funcs(store, name='cactus', plural='cacti')
143
+ func_names = [f.__name__ for f in funcs]
144
+ assert func_names == ['list_cacti', 'get_cactus', 'set_cactus', 'delete_cactus']
145
+
146
+
147
+ def test_invalid_input():
148
+ """Test that invalid inputs raise appropriate errors."""
149
+ with pytest.raises(TypeError):
150
+ mk_mcp_server("not a function")
151
+
152
+ with pytest.raises(TypeError):
153
+ mk_mcp_server([lambda x: x, "not a function"])
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # End-to-end tests (calling tools through the MCP server)
158
+ # ---------------------------------------------------------------------------
159
+
160
+
161
+ def test_e2e_call_tool():
162
+ """End-to-end: call a tool through the MCP server."""
163
+ def add(a: int, b: int) -> int:
164
+ """Add two numbers."""
165
+ return a + b
166
+
167
+ mcp = mk_mcp_server(add)
168
+ assert _call(mcp, 'add', {'a': 3, 'b': 4}) == 7
169
+
170
+
171
+ def test_e2e_multiple_tools():
172
+ """End-to-end: register and call multiple tools."""
173
+ def add(a: int, b: int) -> int:
174
+ """Add."""
175
+ return a + b
176
+
177
+ def multiply(a: int, b: int) -> int:
178
+ """Multiply."""
179
+ return a * b
180
+
181
+ mcp = mk_mcp_server([add, multiply])
182
+
183
+ assert _call(mcp, 'add', {'a': 2, 'b': 3}) == 5
184
+ assert _call(mcp, 'multiply', {'a': 2, 'b': 3}) == 6
185
+
186
+
187
+ def test_e2e_list_tools():
188
+ """End-to-end: list registered tools."""
189
+ def greet(name: str) -> str:
190
+ """Greet someone."""
191
+ return f"Hello, {name}!"
192
+
193
+ def farewell(name: str) -> str:
194
+ """Say goodbye."""
195
+ return f"Goodbye, {name}!"
196
+
197
+ mcp = mk_mcp_server([greet, farewell])
198
+ tools = _run(mcp.list_tools())
199
+ tool_names = {t.name for t in tools}
200
+ assert tool_names == {'greet', 'farewell'}
201
+
202
+
203
+ def test_e2e_store_crud():
204
+ """End-to-end: full CRUD cycle through the MCP server."""
205
+ store = {'a': 1, 'b': 2}
206
+ mcp = mk_mcp_from_store(store, name='item')
207
+
208
+ # List
209
+ keys = _call(mcp, 'list_items')
210
+ assert set(keys) == {'a', 'b'}
211
+
212
+ # Get
213
+ assert _call(mcp, 'get_item', {'key': 'a'}) == 1
214
+
215
+ # Set
216
+ _call(mcp, 'set_item', {'key': 'c', 'value': 3})
217
+ assert store['c'] == 3
218
+
219
+ # Delete
220
+ _call(mcp, 'delete_item', {'key': 'a'})
221
+ assert 'a' not in store
222
+
223
+ # List after mutations
224
+ keys = _call(mcp, 'list_items')
225
+ assert set(keys) == {'b', 'c'}
226
+
227
+
228
+ def test_e2e_input_trans():
229
+ """End-to-end: input transformation through the MCP server."""
230
+ def compute(x: int, y: int) -> int:
231
+ """Compute x + y after transformation."""
232
+ return x + y
233
+
234
+ trans = mk_input_trans({'x': lambda v: v * 10})
235
+ mcp = mk_mcp_server(compute, input_trans=trans)
236
+
237
+ assert _call(mcp, 'compute', {'x': 3, 'y': 1}) == 31
238
+
239
+
240
+ if __name__ == '__main__':
241
+ pytest.main([__file__, '-v'])
py2mcp/trans.py ADDED
@@ -0,0 +1,105 @@
1
+ """Input and output transformation utilities for py2mcp."""
2
+
3
+ from typing import Callable, Mapping, Iterable, Optional, Iterator
4
+
5
+
6
+ def _name_func_pairs_from_mapping(
7
+ name_func_relationships: Mapping,
8
+ ) -> Iterator[tuple[str, Callable]]:
9
+ """Generate (name, func) pairs from various mapping formats.
10
+
11
+ Supports:
12
+ - {name: func} - standard mapping
13
+ - {name: [func1, func2]} - one name, multiple functions
14
+ - {func: name} - reversed mapping
15
+ - {func: [name1, name2]} - one function, multiple names
16
+
17
+ >>> def double(x): return x * 2
18
+ >>> list(_name_func_pairs_from_mapping({'x': double}))
19
+ [('x', <function double at ...>)]
20
+ """
21
+ for k, v in name_func_relationships.items():
22
+ if isinstance(k, str):
23
+ name = k
24
+ if callable(v):
25
+ yield name, v
26
+ elif isinstance(v, Iterable):
27
+ for func in v:
28
+ if not callable(func):
29
+ raise TypeError(f"Expected callable, got {type(func)}")
30
+ yield name, func
31
+ else:
32
+ raise TypeError(f"Value must be callable or iterable of callables: {v}")
33
+ elif callable(k):
34
+ func = k
35
+ if isinstance(v, str):
36
+ yield v, func
37
+ elif isinstance(v, Iterable):
38
+ for name in v:
39
+ if not isinstance(name, str):
40
+ raise TypeError(f"Expected string, got {type(name)}")
41
+ yield name, func
42
+ else:
43
+ raise TypeError(f"Value must be string or iterable of strings: {v}")
44
+ else:
45
+ raise TypeError(f"Key must be string or callable: {k}")
46
+
47
+
48
+ def _to_name_func_map(name_func_relationships: Mapping) -> dict[str, Callable]:
49
+ """Convert name-func relationships to a simple name->func mapping.
50
+
51
+ >>> def double(x): return x * 2
52
+ >>> _to_name_func_map({'x': double})
53
+ {'x': <function double at ...>}
54
+ """
55
+ if not isinstance(name_func_relationships, Mapping):
56
+ raise TypeError(
57
+ f"Expected Mapping of name:func or func:name pairs, got {type(name_func_relationships)}"
58
+ )
59
+
60
+ pairs = list(_name_func_pairs_from_mapping(name_func_relationships))
61
+ result = dict(pairs)
62
+
63
+ if len(result) != len(pairs):
64
+ raise ValueError("Duplicate names found in relationships")
65
+
66
+ return result
67
+
68
+
69
+ def _apply_transformations(
70
+ kwargs: dict, name_func_map: Mapping[str, Callable]
71
+ ) -> Iterator[tuple[str, any]]:
72
+ """Apply transformations to matching kwargs.
73
+
74
+ >>> def double(x): return x * 2
75
+ >>> transforms = {'a': double}
76
+ >>> dict(_apply_transformations({'a': 5, 'b': 10}, transforms))
77
+ {'a': 10, 'b': 10}
78
+ """
79
+ for name, value in kwargs.items():
80
+ if name in name_func_map:
81
+ yield name, name_func_map[name](value)
82
+ else:
83
+ yield name, value
84
+
85
+
86
+ def mk_input_trans(
87
+ name_func_relationships: Optional[Mapping] = None,
88
+ ) -> Callable[[dict], dict]:
89
+ """Create an input transformation function from name->func mappings.
90
+
91
+ >>> def to_int(x): return int(x)
92
+ >>> trans = mk_input_trans({'x': to_int})
93
+ >>> trans({'x': '42', 'y': 'hello'})
94
+ {'x': 42, 'y': 'hello'}
95
+ """
96
+ if name_func_relationships is None:
97
+ return lambda kwargs: dict(kwargs)
98
+
99
+ name_func_map = _to_name_func_map(name_func_relationships)
100
+
101
+ def input_trans(kwargs: dict) -> dict:
102
+ """Transform input kwargs according to the mapping."""
103
+ return dict(_apply_transformations(kwargs, name_func_map))
104
+
105
+ return input_trans
py2mcp/util.py ADDED
@@ -0,0 +1,70 @@
1
+ """General utilities for py2mcp."""
2
+
3
+ from typing import MutableMapping, Callable, TypeVar
4
+ from collections.abc import Iterator
5
+
6
+ KT = TypeVar("KT")
7
+ VT = TypeVar("VT")
8
+
9
+
10
+ def _store_to_funcs(
11
+ store: MutableMapping[KT, VT],
12
+ *,
13
+ singular: str = "item",
14
+ plural: str = "",
15
+ ) -> Iterator[tuple[str, Callable]]:
16
+ """Generate CRUD functions from a MutableMapping.
17
+
18
+ >>> store = {'a': 1, 'b': 2}
19
+ >>> funcs = dict(_store_to_funcs(store, singular='item'))
20
+ >>> sorted(funcs['list_items']())
21
+ ['a', 'b']
22
+ >>> funcs['get_item']('a')
23
+ 1
24
+ """
25
+ plural = plural or f"{singular}s"
26
+
27
+ def list_items() -> list[KT]:
28
+ """List all keys."""
29
+ return list(store.keys())
30
+
31
+ def get_item(key: KT) -> VT:
32
+ """Get a value by key."""
33
+ return store[key]
34
+
35
+ def set_item(key: KT, value: VT) -> str:
36
+ """Set a value."""
37
+ store[key] = value
38
+ return f"Set {singular} '{key}'"
39
+
40
+ def delete_item(key: KT) -> str:
41
+ """Delete a value."""
42
+ del store[key]
43
+ return f"Deleted {singular} '{key}'"
44
+
45
+ for func, func_name in [
46
+ (list_items, f"list_{plural}"),
47
+ (get_item, f"get_{singular}"),
48
+ (set_item, f"set_{singular}"),
49
+ (delete_item, f"delete_{singular}"),
50
+ ]:
51
+ func.__name__ = func_name
52
+ yield func_name, func
53
+
54
+
55
+ def store_to_funcs(
56
+ store: MutableMapping[KT, VT],
57
+ *,
58
+ name: str = "item",
59
+ plural: str = "",
60
+ ) -> list[Callable]:
61
+ """Convert a MutableMapping into CRUD functions.
62
+
63
+ >>> projects = {'p1': {'name': 'Project 1'}}
64
+ >>> funcs = store_to_funcs(projects, name='project')
65
+ >>> len(funcs)
66
+ 4
67
+ >>> [f.__name__ for f in funcs]
68
+ ['list_projects', 'get_project', 'set_project', 'delete_project']
69
+ """
70
+ return [func for _, func in _store_to_funcs(store, singular=name, plural=plural)]
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: py2mcp
3
+ Version: 0.1.1
4
+ Summary: Quick MCP server creation from Python functions
5
+ Project-URL: Homepage, https://github.com/i2mint/py2mcp
6
+ Project-URL: Repository, https://github.com/i2mint/py2mcp
7
+ Project-URL: Documentation, https://i2mint.github.io/py2mcp
8
+ Author: Thor Whalen
9
+ License: mit
10
+ License-File: LICENSE
11
+ Keywords: ai,llm,mcp,model-context-protocol
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: fastmcp
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
16
+ Requires-Dist: pytest>=7.0; extra == 'dev'
17
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
18
+ Provides-Extra: docs
19
+ Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
20
+ Requires-Dist: sphinx>=6.0; extra == 'docs'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # py2mcp
24
+
25
+ Quick MCP (Model Context Protocol) server creation from Python functions.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install py2mcp
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```python
36
+ from py2mcp import mk_mcp_server
37
+
38
+ def add(a: int, b: int) -> int:
39
+ """Add two numbers"""
40
+ return a + b
41
+
42
+ def greet(name: str = "world") -> str:
43
+ """Greet someone"""
44
+ return f"Hello, {name}!"
45
+
46
+ # Create and run MCP server
47
+ mcp = mk_mcp_server([add, greet])
48
+
49
+ if __name__ == '__main__':
50
+ mcp.run()
51
+ ```
52
+
53
+ That's it! Your functions are now available as MCP tools.
54
+
55
+ ## Features
56
+
57
+ - **Simple**: Just pass functions to `mk_mcp_server()`
58
+ - **Flexible**: Supports input/output transformations
59
+ - **Pythonic**: Clean, decorator-free function definitions
60
+ - **Powerful**: Built on FastMCP for production-ready servers
61
+
62
+ ## Input Transformations
63
+
64
+ Transform inputs before they reach your functions:
65
+
66
+ ```python
67
+ from py2mcp import mk_mcp_server, mk_input_trans
68
+ import numpy as np
69
+
70
+ def add_arrays(a, b):
71
+ """Add two numpy arrays"""
72
+ return (a + b).tolist()
73
+
74
+ # Convert list inputs to numpy arrays
75
+ input_trans = mk_input_trans({'a': np.array, 'b': np.array})
76
+ mcp = mk_mcp_server([add_arrays], input_trans=input_trans)
77
+ ```
78
+
79
+ ## From Stores (MutableMapping)
80
+
81
+ Automatically expose CRUD operations from any mapping:
82
+
83
+ ```python
84
+ from py2mcp import mk_mcp_from_store
85
+
86
+ projects = {'proj1': {'name': 'Project 1'}, 'proj2': {'name': 'Project 2'}}
87
+ mcp = mk_mcp_from_store(projects, name="project")
88
+
89
+ # Automatically creates: list_projects, get_project, set_project, delete_project
90
+ ```
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,10 @@
1
+ py2mcp/__init__.py,sha256=O6zE924L-4RGq00DxbVaXVXMn7l7eITQfkTLTM41Dj0,743
2
+ py2mcp/base.py,sha256=uFYe9dN-2Yr0MRV2Dzv0sT3M5awUwPhRoXA_SYwdpLg,1426
3
+ py2mcp/main.py,sha256=ZYjZWZhRFkvtZjwOxVda07GuXKkc7Qg742nH76tTSrE,2739
4
+ py2mcp/trans.py,sha256=pj53X58DIE2ToaY2z3ZM6673d53kEAI-YfTsvs6BOFE,3571
5
+ py2mcp/util.py,sha256=VNk_akypXYH3FqVVuxFEG897KyjQOnxoXZtQ4iflmf8,1853
6
+ py2mcp/tests/test_basic.py,sha256=F-bVB-VLXPYlVC557Yocmkm009yA_6Ol5_T3NUUNw0U,6642
7
+ py2mcp-0.1.1.dist-info/METADATA,sha256=5fvf_4AxUvW1QXRehHERbXTq9MpJoKq9Cq22XblXHTM,2283
8
+ py2mcp-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ py2mcp-0.1.1.dist-info/licenses/LICENSE,sha256=ekE13yCE9fWWEJfP1NPnUJabZ_Wwrc6coKJV2ZZx27w,1068
10
+ py2mcp-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thor Whalen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.