py2mcp 0.1.1__tar.gz
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-0.1.1/.gitattributes +1 -0
- py2mcp-0.1.1/.github/workflows/ci.yml +256 -0
- py2mcp-0.1.1/.gitignore +121 -0
- py2mcp-0.1.1/COMPARISON.md +346 -0
- py2mcp-0.1.1/GETTING_STARTED.md +286 -0
- py2mcp-0.1.1/INDEX.md +201 -0
- py2mcp-0.1.1/LICENSE +21 -0
- py2mcp-0.1.1/PKG-INFO +94 -0
- py2mcp-0.1.1/PROJECT_SUMMARY.md +302 -0
- py2mcp-0.1.1/README.md +72 -0
- py2mcp-0.1.1/USAGE_GUIDE.md +331 -0
- py2mcp-0.1.1/examples/simple.py +85 -0
- py2mcp-0.1.1/examples/store_example.py +54 -0
- py2mcp-0.1.1/examples/transformations.py +96 -0
- py2mcp-0.1.1/misc/CHANGELOG.md +34 -0
- py2mcp-0.1.1/py2mcp/__init__.py +27 -0
- py2mcp-0.1.1/py2mcp/base.py +49 -0
- py2mcp-0.1.1/py2mcp/main.py +91 -0
- py2mcp-0.1.1/py2mcp/tests/test_basic.py +241 -0
- py2mcp-0.1.1/py2mcp/trans.py +105 -0
- py2mcp-0.1.1/py2mcp/util.py +70 -0
- py2mcp-0.1.1/pyproject.toml +161 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.ipynb linguist-documentation
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
name: Continuous Integration (uv)
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
|
|
4
|
+
# Note: Environment variables (PROJECT_NAME and vars from [tool.wads.ci.env])
|
|
5
|
+
# are set by the read-ci-config action in the setup job and made available
|
|
6
|
+
# to all subsequent jobs via GITHUB_ENV
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
# First job: Read configuration from pyproject.toml
|
|
10
|
+
setup:
|
|
11
|
+
name: Read Configuration
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
outputs:
|
|
14
|
+
project-name: ${{ steps.config.outputs.project-name }}
|
|
15
|
+
python-versions: ${{ steps.config.outputs.python-versions }}
|
|
16
|
+
pytest-args: ${{ steps.config.outputs.pytest-args }}
|
|
17
|
+
coverage-enabled: ${{ steps.config.outputs.coverage-enabled }}
|
|
18
|
+
exclude-paths: ${{ steps.config.outputs.exclude-paths }}
|
|
19
|
+
test-on-windows: ${{ steps.config.outputs.test-on-windows }}
|
|
20
|
+
build-sdist: ${{ steps.config.outputs.build-sdist }}
|
|
21
|
+
build-wheel: ${{ steps.config.outputs.build-wheel }}
|
|
22
|
+
metrics-enabled: ${{ steps.config.outputs.metrics-enabled }}
|
|
23
|
+
metrics-config-path: ${{ steps.config.outputs.metrics-config-path }}
|
|
24
|
+
metrics-storage-branch: ${{ steps.config.outputs.metrics-storage-branch }}
|
|
25
|
+
metrics-python-version: ${{ steps.config.outputs.metrics-python-version }}
|
|
26
|
+
metrics-force-run: ${{ steps.config.outputs.metrics-force-run }}
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
|
|
31
|
+
- name: Set up uv
|
|
32
|
+
uses: astral-sh/setup-uv@v5
|
|
33
|
+
|
|
34
|
+
- name: Set up Python
|
|
35
|
+
run: uv python install 3.11
|
|
36
|
+
|
|
37
|
+
- name: Read CI Config
|
|
38
|
+
id: config
|
|
39
|
+
uses: i2mint/wads/actions/read-ci-config@master
|
|
40
|
+
with:
|
|
41
|
+
pyproject-path: .
|
|
42
|
+
|
|
43
|
+
# Second job: Validation using the config
|
|
44
|
+
validation:
|
|
45
|
+
name: Validation
|
|
46
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
|
47
|
+
needs: setup
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
strategy:
|
|
50
|
+
matrix:
|
|
51
|
+
python-version: ${{ fromJson(needs.setup.outputs.python-versions) }}
|
|
52
|
+
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
|
|
56
|
+
- name: Set up uv
|
|
57
|
+
uses: astral-sh/setup-uv@v5
|
|
58
|
+
with:
|
|
59
|
+
enable-cache: true
|
|
60
|
+
|
|
61
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
62
|
+
run: |
|
|
63
|
+
uv python install ${{ matrix.python-version }}
|
|
64
|
+
uv venv --python ${{ matrix.python-version }}
|
|
65
|
+
|
|
66
|
+
- name: Install System Dependencies
|
|
67
|
+
uses: i2mint/wads/actions/install-system-deps@master
|
|
68
|
+
with:
|
|
69
|
+
pyproject-path: .
|
|
70
|
+
|
|
71
|
+
- name: Install Dependencies
|
|
72
|
+
run: |
|
|
73
|
+
source .venv/bin/activate
|
|
74
|
+
uv pip install -e ".[dev]"
|
|
75
|
+
|
|
76
|
+
- name: Format Source Code
|
|
77
|
+
run: uvx ruff format .
|
|
78
|
+
|
|
79
|
+
- name: Lint Validation
|
|
80
|
+
run: uvx ruff check --output-format=github ${{ needs.setup.outputs.project-name }}
|
|
81
|
+
|
|
82
|
+
- name: Run Tests
|
|
83
|
+
run: |
|
|
84
|
+
source .venv/bin/activate
|
|
85
|
+
PYTEST_ARGS="${{ needs.setup.outputs.pytest-args }}"
|
|
86
|
+
EXCLUDE="${{ needs.setup.outputs.exclude-paths }}"
|
|
87
|
+
COVERAGE="${{ needs.setup.outputs.coverage-enabled }}"
|
|
88
|
+
ROOT_DIR="${{ needs.setup.outputs.project-name }}"
|
|
89
|
+
|
|
90
|
+
CMD="python -m pytest"
|
|
91
|
+
|
|
92
|
+
# Add coverage flags
|
|
93
|
+
if [ "$COVERAGE" = "true" ]; then
|
|
94
|
+
uv pip install pytest-cov
|
|
95
|
+
CMD="$CMD --cov=$ROOT_DIR --cov-report=term-missing"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Add doctest flags
|
|
99
|
+
CMD="$CMD --doctest-modules"
|
|
100
|
+
CMD="$CMD -o doctest_optionflags='ELLIPSIS IGNORE_EXCEPTION_DETAIL'"
|
|
101
|
+
|
|
102
|
+
# Add exclude paths
|
|
103
|
+
if [ -n "$EXCLUDE" ]; then
|
|
104
|
+
IFS=',' read -ra PATHS <<< "$EXCLUDE"
|
|
105
|
+
for path in "${PATHS[@]}"; do
|
|
106
|
+
path=$(echo "$path" | xargs)
|
|
107
|
+
CMD="$CMD --ignore=$path"
|
|
108
|
+
done
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# Add extra pytest args
|
|
112
|
+
if [ -n "$PYTEST_ARGS" ]; then
|
|
113
|
+
CMD="$CMD $PYTEST_ARGS"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
echo "Running: $CMD"
|
|
117
|
+
eval $CMD
|
|
118
|
+
|
|
119
|
+
- name: Track Code Metrics
|
|
120
|
+
if: needs.setup.outputs.metrics-enabled == 'true'
|
|
121
|
+
uses: i2mint/umpyre/actions/track-metrics@master
|
|
122
|
+
continue-on-error: true
|
|
123
|
+
with:
|
|
124
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
125
|
+
config-path: ${{ needs.setup.outputs.metrics-config-path }}
|
|
126
|
+
storage-branch: ${{ needs.setup.outputs.metrics-storage-branch }}
|
|
127
|
+
python-version: ${{ needs.setup.outputs.metrics-python-version }}
|
|
128
|
+
force-run: ${{ needs.setup.outputs.metrics-force-run }}
|
|
129
|
+
|
|
130
|
+
# Optional Windows testing (if enabled in config)
|
|
131
|
+
windows-validation:
|
|
132
|
+
name: Windows Tests
|
|
133
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]') && needs.setup.outputs.test-on-windows == 'true'"
|
|
134
|
+
needs: setup
|
|
135
|
+
runs-on: windows-latest
|
|
136
|
+
continue-on-error: true
|
|
137
|
+
|
|
138
|
+
steps:
|
|
139
|
+
- uses: actions/checkout@v4
|
|
140
|
+
|
|
141
|
+
- name: Set up uv
|
|
142
|
+
uses: astral-sh/setup-uv@v5
|
|
143
|
+
with:
|
|
144
|
+
enable-cache: true
|
|
145
|
+
|
|
146
|
+
- name: Set up Python
|
|
147
|
+
run: |
|
|
148
|
+
uv python install ${{ fromJson(needs.setup.outputs.python-versions)[0] }}
|
|
149
|
+
uv venv
|
|
150
|
+
|
|
151
|
+
- name: Install System Dependencies
|
|
152
|
+
uses: i2mint/wads/actions/install-system-deps@master
|
|
153
|
+
with:
|
|
154
|
+
pyproject-path: .
|
|
155
|
+
|
|
156
|
+
- name: Install Dependencies
|
|
157
|
+
run: |
|
|
158
|
+
.venv\Scripts\activate
|
|
159
|
+
uv pip install -e ".[dev]"
|
|
160
|
+
|
|
161
|
+
- name: Run tests
|
|
162
|
+
id: test
|
|
163
|
+
continue-on-error: true
|
|
164
|
+
run: |
|
|
165
|
+
.venv\Scripts\activate
|
|
166
|
+
pytest
|
|
167
|
+
|
|
168
|
+
- name: Report test results
|
|
169
|
+
if: always()
|
|
170
|
+
run: |
|
|
171
|
+
if ("${{ steps.test.outcome }}" -eq "failure") {
|
|
172
|
+
echo "::warning::Windows tests failed but workflow continues"
|
|
173
|
+
echo "## ⚠️ Windows Tests Failed" >> $env:GITHUB_STEP_SUMMARY
|
|
174
|
+
echo "Tests failed on Windows but this is informational only." >> $env:GITHUB_STEP_SUMMARY
|
|
175
|
+
} else {
|
|
176
|
+
echo "## ✅ Windows Tests Passed" >> $env:GITHUB_STEP_SUMMARY
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Publishing job
|
|
180
|
+
publish:
|
|
181
|
+
name: Publish
|
|
182
|
+
permissions:
|
|
183
|
+
contents: write
|
|
184
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]') && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')"
|
|
185
|
+
needs: [setup, validation]
|
|
186
|
+
runs-on: ubuntu-latest
|
|
187
|
+
|
|
188
|
+
steps:
|
|
189
|
+
- uses: actions/checkout@v4
|
|
190
|
+
with:
|
|
191
|
+
fetch-depth: 0
|
|
192
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
193
|
+
|
|
194
|
+
- name: Set up uv
|
|
195
|
+
uses: astral-sh/setup-uv@v5
|
|
196
|
+
|
|
197
|
+
- name: Set up Python
|
|
198
|
+
run: uv python install ${{ fromJson(needs.setup.outputs.python-versions)[0] }}
|
|
199
|
+
|
|
200
|
+
- name: Format Source Code
|
|
201
|
+
run: uvx ruff format .
|
|
202
|
+
|
|
203
|
+
- name: Update Version Number
|
|
204
|
+
id: version
|
|
205
|
+
uses: i2mint/isee/actions/bump-version-number@master
|
|
206
|
+
|
|
207
|
+
- name: Build Distribution
|
|
208
|
+
run: |
|
|
209
|
+
BUILD_ARGS=""
|
|
210
|
+
if [ "${{ needs.setup.outputs.build-sdist }}" = "false" ]; then
|
|
211
|
+
BUILD_ARGS="$BUILD_ARGS --no-sdist"
|
|
212
|
+
fi
|
|
213
|
+
if [ "${{ needs.setup.outputs.build-wheel }}" = "false" ]; then
|
|
214
|
+
BUILD_ARGS="$BUILD_ARGS --no-wheel"
|
|
215
|
+
fi
|
|
216
|
+
uv build $BUILD_ARGS
|
|
217
|
+
|
|
218
|
+
- name: Publish to PyPI
|
|
219
|
+
env:
|
|
220
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_PASSWORD }}
|
|
221
|
+
run: uv publish dist/*
|
|
222
|
+
|
|
223
|
+
- name: Force SSH for git remote
|
|
224
|
+
run: |
|
|
225
|
+
git remote set-url origin git@github.com:${{ github.repository }}.git
|
|
226
|
+
|
|
227
|
+
- name: Commit Changes
|
|
228
|
+
uses: i2mint/wads/actions/git-commit@master
|
|
229
|
+
with:
|
|
230
|
+
commit-message: "**CI** Formatted code + Updated version to ${{ env.VERSION }} [skip ci]"
|
|
231
|
+
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
232
|
+
push: true
|
|
233
|
+
|
|
234
|
+
- name: Tag Repository
|
|
235
|
+
uses: i2mint/wads/actions/git-tag@master
|
|
236
|
+
with:
|
|
237
|
+
tag: ${{ env.VERSION }}
|
|
238
|
+
message: "Release version ${{ env.VERSION }}"
|
|
239
|
+
push: true
|
|
240
|
+
|
|
241
|
+
# Optional GitHub Pages
|
|
242
|
+
github-pages:
|
|
243
|
+
name: Publish GitHub Pages
|
|
244
|
+
permissions:
|
|
245
|
+
contents: write
|
|
246
|
+
pages: write
|
|
247
|
+
id-token: write
|
|
248
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]') && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)"
|
|
249
|
+
needs: publish
|
|
250
|
+
runs-on: ubuntu-latest
|
|
251
|
+
|
|
252
|
+
steps:
|
|
253
|
+
- uses: i2mint/epythet/actions/publish-github-pages@master
|
|
254
|
+
with:
|
|
255
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
256
|
+
ignore: "tests/,scrap/,examples/"
|
py2mcp-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
wads_configs.json
|
|
2
|
+
data/wads_configs.json
|
|
3
|
+
wads/data/wads_configs.json
|
|
4
|
+
|
|
5
|
+
# Byte-compiled / optimized / DLL files
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*$py.class
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
.DS_Store
|
|
12
|
+
# C extensions
|
|
13
|
+
*.so
|
|
14
|
+
|
|
15
|
+
# TLS certificates
|
|
16
|
+
## Ignore all PEM files anywhere
|
|
17
|
+
*.pem
|
|
18
|
+
## Also ignore any certs directory
|
|
19
|
+
certs/
|
|
20
|
+
|
|
21
|
+
# Distribution / packaging
|
|
22
|
+
.Python
|
|
23
|
+
build/
|
|
24
|
+
develop-eggs/
|
|
25
|
+
dist/
|
|
26
|
+
downloads/
|
|
27
|
+
eggs/
|
|
28
|
+
.eggs/
|
|
29
|
+
lib/
|
|
30
|
+
lib64/
|
|
31
|
+
parts/
|
|
32
|
+
sdist/
|
|
33
|
+
var/
|
|
34
|
+
wheels/
|
|
35
|
+
*.egg-info/
|
|
36
|
+
.installed.cfg
|
|
37
|
+
*.egg
|
|
38
|
+
MANIFEST
|
|
39
|
+
_build
|
|
40
|
+
|
|
41
|
+
# PyInstaller
|
|
42
|
+
# Usually these files are written by a python script from a template
|
|
43
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
44
|
+
*.manifest
|
|
45
|
+
*.spec
|
|
46
|
+
|
|
47
|
+
# Installer logs
|
|
48
|
+
pip-log.txt
|
|
49
|
+
pip-delete-this-directory.txt
|
|
50
|
+
|
|
51
|
+
# Unit test / coverage reports
|
|
52
|
+
htmlcov/
|
|
53
|
+
.tox/
|
|
54
|
+
.coverage
|
|
55
|
+
.coverage.*
|
|
56
|
+
.cache
|
|
57
|
+
nosetests.xml
|
|
58
|
+
coverage.xml
|
|
59
|
+
*.cover
|
|
60
|
+
.hypothesis/
|
|
61
|
+
.pytest_cache/
|
|
62
|
+
|
|
63
|
+
# Translations
|
|
64
|
+
*.mo
|
|
65
|
+
*.pot
|
|
66
|
+
|
|
67
|
+
# Django stuff:
|
|
68
|
+
*.log
|
|
69
|
+
local_settings.py
|
|
70
|
+
db.sqlite3
|
|
71
|
+
|
|
72
|
+
# Flask stuff:
|
|
73
|
+
instance/
|
|
74
|
+
.webassets-cache
|
|
75
|
+
|
|
76
|
+
# Scrapy stuff:
|
|
77
|
+
.scrapy
|
|
78
|
+
|
|
79
|
+
# Sphinx documentation
|
|
80
|
+
docs/_build/
|
|
81
|
+
docs/*
|
|
82
|
+
|
|
83
|
+
# PyBuilder
|
|
84
|
+
target/
|
|
85
|
+
|
|
86
|
+
# Jupyter Notebook
|
|
87
|
+
.ipynb_checkpoints
|
|
88
|
+
|
|
89
|
+
# pyenv
|
|
90
|
+
.python-version
|
|
91
|
+
|
|
92
|
+
# celery beat schedule file
|
|
93
|
+
celerybeat-schedule
|
|
94
|
+
|
|
95
|
+
# SageMath parsed files
|
|
96
|
+
*.sage.py
|
|
97
|
+
|
|
98
|
+
# Environments
|
|
99
|
+
.env
|
|
100
|
+
.venv
|
|
101
|
+
env/
|
|
102
|
+
venv/
|
|
103
|
+
ENV/
|
|
104
|
+
env.bak/
|
|
105
|
+
venv.bak/
|
|
106
|
+
|
|
107
|
+
# Spyder project settings
|
|
108
|
+
.spyderproject
|
|
109
|
+
.spyproject
|
|
110
|
+
|
|
111
|
+
# Rope project settings
|
|
112
|
+
.ropeproject
|
|
113
|
+
|
|
114
|
+
# mkdocs documentation
|
|
115
|
+
/site
|
|
116
|
+
|
|
117
|
+
# mypy
|
|
118
|
+
.mypy_cache/
|
|
119
|
+
|
|
120
|
+
# PyCharm
|
|
121
|
+
.idea
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# py2mcp vs qh (py2http) - Pattern Comparison
|
|
2
|
+
|
|
3
|
+
This document shows how `py2mcp` follows the same design patterns as `qh` (the HTTP service builder), making it familiar and intuitive.
|
|
4
|
+
|
|
5
|
+
## Core Pattern: Functions → Server
|
|
6
|
+
|
|
7
|
+
### qh (HTTP)
|
|
8
|
+
```python
|
|
9
|
+
from qh import mk_http_service_app
|
|
10
|
+
|
|
11
|
+
def add(a: int, b: int) -> int:
|
|
12
|
+
return a + b
|
|
13
|
+
|
|
14
|
+
app = mk_http_service_app([add])
|
|
15
|
+
app.run()
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### py2mcp (MCP)
|
|
19
|
+
```python
|
|
20
|
+
from py2mcp import mk_mcp_server
|
|
21
|
+
|
|
22
|
+
def add(a: int, b: int) -> int:
|
|
23
|
+
return a + b
|
|
24
|
+
|
|
25
|
+
mcp = mk_mcp_server([add])
|
|
26
|
+
mcp.run()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Identical pattern!** Pass functions, get a server.
|
|
30
|
+
|
|
31
|
+
## Input Transformations
|
|
32
|
+
|
|
33
|
+
### qh (HTTP)
|
|
34
|
+
```python
|
|
35
|
+
from qh import mk_http_service_app
|
|
36
|
+
from qh.trans import mk_json_handler_from_name_mapping
|
|
37
|
+
import numpy as np
|
|
38
|
+
|
|
39
|
+
def add_arrays(a, b):
|
|
40
|
+
return (a + b).tolist()
|
|
41
|
+
|
|
42
|
+
input_trans = mk_json_handler_from_name_mapping({
|
|
43
|
+
'a': np.array,
|
|
44
|
+
'b': np.array
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
app = mk_http_service_app([add_arrays], input_trans=input_trans)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### py2mcp (MCP)
|
|
51
|
+
```python
|
|
52
|
+
from py2mcp import mk_mcp_server, mk_input_trans
|
|
53
|
+
import numpy as np
|
|
54
|
+
|
|
55
|
+
def add_arrays(a, b):
|
|
56
|
+
return (a + b).tolist()
|
|
57
|
+
|
|
58
|
+
input_trans = mk_input_trans({
|
|
59
|
+
'a': np.array,
|
|
60
|
+
'b': np.array
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
mcp = mk_mcp_server([add_arrays], input_trans=input_trans)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Same transformation pattern!** Map parameter names to conversion functions.
|
|
67
|
+
|
|
68
|
+
## Store/Mapping Dispatch
|
|
69
|
+
|
|
70
|
+
### qh (HTTP) - Conceptual
|
|
71
|
+
```python
|
|
72
|
+
# qh has store dispatch patterns (see scrap/store_dispatch_*.py)
|
|
73
|
+
# where a MutableMapping is exposed via HTTP endpoints
|
|
74
|
+
|
|
75
|
+
from qh.scrap.store_dispatch_1 import StoreAccess
|
|
76
|
+
|
|
77
|
+
store = StoreAccess.from_uri('test_uri')
|
|
78
|
+
# Exposes: list(), read(), write(), delete()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### py2mcp (MCP)
|
|
82
|
+
```python
|
|
83
|
+
from py2mcp import mk_mcp_from_store
|
|
84
|
+
|
|
85
|
+
projects = {'proj1': {...}, 'proj2': {...}}
|
|
86
|
+
|
|
87
|
+
mcp = mk_mcp_from_store(projects, name='project')
|
|
88
|
+
# Automatically creates:
|
|
89
|
+
# - list_projects()
|
|
90
|
+
# - get_project(key)
|
|
91
|
+
# - set_project(key, value)
|
|
92
|
+
# - delete_project(key)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Same CRUD pattern!** Automatically generate operations from stores.
|
|
96
|
+
|
|
97
|
+
## Architecture Parallels
|
|
98
|
+
|
|
99
|
+
| Aspect | qh (py2http) | py2mcp |
|
|
100
|
+
|--------|--------------|---------|
|
|
101
|
+
| **Foundation** | py2http (bottle/FastAPI) | FastMCP |
|
|
102
|
+
| **Philosophy** | Don't reinvent HTTP | Don't reinvent MCP |
|
|
103
|
+
| **Main Function** | `mk_http_service_app()` | `mk_mcp_server()` |
|
|
104
|
+
| **Transformation** | `mk_json_handler_from_name_mapping()` | `mk_input_trans()` |
|
|
105
|
+
| **Store Support** | Store dispatch examples | `mk_mcp_from_store()` |
|
|
106
|
+
| **Return Type** | HTTP app object | FastMCP object |
|
|
107
|
+
| **Run Method** | `app.run()` | `mcp.run()` |
|
|
108
|
+
|
|
109
|
+
## Module Structure Comparison
|
|
110
|
+
|
|
111
|
+
### qh Structure
|
|
112
|
+
```
|
|
113
|
+
qh/
|
|
114
|
+
├── __init__.py # Exports: mk_http_service_app
|
|
115
|
+
├── main.py # Core app creation
|
|
116
|
+
├── trans.py # Transformations
|
|
117
|
+
├── util.py # Helpers (flat_callable_for, etc.)
|
|
118
|
+
└── scrap/ # WIP patterns
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### py2mcp Structure
|
|
122
|
+
```
|
|
123
|
+
py2mcp/
|
|
124
|
+
├── __init__.py # Exports: mk_mcp_server, mk_input_trans, mk_mcp_from_store
|
|
125
|
+
├── main.py # Core server creation
|
|
126
|
+
├── trans.py # Transformations
|
|
127
|
+
├── util.py # Helpers (store_to_funcs, etc.)
|
|
128
|
+
└── tests/ # Test suite
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Nearly identical organization!**
|
|
132
|
+
|
|
133
|
+
## Implementation Details
|
|
134
|
+
|
|
135
|
+
### Function Flattening
|
|
136
|
+
|
|
137
|
+
Both packages handle methods and functions uniformly:
|
|
138
|
+
|
|
139
|
+
#### qh
|
|
140
|
+
```python
|
|
141
|
+
# from qh.util import flat_callable_for
|
|
142
|
+
def flat_callable_for(func, func_name=None, cls=None):
|
|
143
|
+
"""Flatten cls->instance->method call pipeline"""
|
|
144
|
+
containing_cls = get_class_that_defined_method(func)
|
|
145
|
+
if not containing_cls:
|
|
146
|
+
return func # Already flat
|
|
147
|
+
# ... flatten the method
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### py2mcp
|
|
151
|
+
```python
|
|
152
|
+
# from py2mcp.base import _normalize_to_iterable
|
|
153
|
+
def _normalize_to_iterable(funcs):
|
|
154
|
+
"""Normalize input to an iterable of callables"""
|
|
155
|
+
if callable(funcs):
|
|
156
|
+
return [funcs]
|
|
157
|
+
elif isinstance(funcs, Iterable):
|
|
158
|
+
return list(funcs)
|
|
159
|
+
# ... validate
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Transformation Pipeline
|
|
163
|
+
|
|
164
|
+
Both use the same pattern: **name → function mapping**
|
|
165
|
+
|
|
166
|
+
#### qh
|
|
167
|
+
```python
|
|
168
|
+
def transform_mapping_vals_with_name_func_map(mapping, name_func_map):
|
|
169
|
+
for name, val in mapping.items():
|
|
170
|
+
if name in name_func_map:
|
|
171
|
+
yield name, name_func_map[name](val)
|
|
172
|
+
else:
|
|
173
|
+
yield name, val
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### py2mcp
|
|
177
|
+
```python
|
|
178
|
+
def _apply_transformations(kwargs, name_func_map):
|
|
179
|
+
for name, value in kwargs.items():
|
|
180
|
+
if name in name_func_map:
|
|
181
|
+
yield name, name_func_map[name](value)
|
|
182
|
+
else:
|
|
183
|
+
yield name, value
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Identical logic!**
|
|
187
|
+
|
|
188
|
+
## Key Differences
|
|
189
|
+
|
|
190
|
+
While the patterns are the same, there are protocol differences:
|
|
191
|
+
|
|
192
|
+
| Feature | HTTP (qh) | MCP (py2mcp) |
|
|
193
|
+
|---------|-----------|--------------|
|
|
194
|
+
| **Transport** | HTTP endpoints | Stdio/HTTP/SSE |
|
|
195
|
+
| **Client** | curl, browsers | Claude, Cursor, MCP clients |
|
|
196
|
+
| **Method Types** | GET/POST/PUT/DELETE | Tools/Resources/Prompts |
|
|
197
|
+
| **Request Format** | JSON body, URL params | JSON-RPC |
|
|
198
|
+
| **Response Format** | JSON response | Structured tool results |
|
|
199
|
+
|
|
200
|
+
## Usage Comparison
|
|
201
|
+
|
|
202
|
+
### Starting a Server
|
|
203
|
+
|
|
204
|
+
#### qh
|
|
205
|
+
```python
|
|
206
|
+
# HTTP server on port 8080
|
|
207
|
+
if __name__ == '__main__':
|
|
208
|
+
app.run(port=8080)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Test with:
|
|
212
|
+
```bash
|
|
213
|
+
curl -X POST http://localhost:8080/add \
|
|
214
|
+
-H "Content-Type: application/json" \
|
|
215
|
+
-d '{"a": 3, "b": 5}'
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### py2mcp
|
|
219
|
+
```python
|
|
220
|
+
# Stdio server (default) or HTTP
|
|
221
|
+
if __name__ == '__main__':
|
|
222
|
+
mcp.run() # stdio
|
|
223
|
+
# mcp.run(transport='http', port=8000) # http
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Test with:
|
|
227
|
+
```bash
|
|
228
|
+
fastmcp dev server.py # Opens web inspector
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Function Requirements
|
|
232
|
+
|
|
233
|
+
Both require:
|
|
234
|
+
- Type hints (for schema generation)
|
|
235
|
+
- Docstrings (for documentation)
|
|
236
|
+
- JSON-serializable returns
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
# Works in both qh and py2mcp
|
|
240
|
+
def process_data(
|
|
241
|
+
text: str,
|
|
242
|
+
count: int = 10,
|
|
243
|
+
uppercase: bool = False
|
|
244
|
+
) -> dict:
|
|
245
|
+
"""Process text data.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
text: Input text
|
|
249
|
+
count: Max length
|
|
250
|
+
uppercase: Convert to uppercase
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Processed result
|
|
254
|
+
"""
|
|
255
|
+
result = text[:count]
|
|
256
|
+
if uppercase:
|
|
257
|
+
result = result.upper()
|
|
258
|
+
return {"result": result, "length": len(result)}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Migration Guide: HTTP → MCP
|
|
262
|
+
|
|
263
|
+
If you have a qh HTTP service and want to make it an MCP server:
|
|
264
|
+
|
|
265
|
+
1. **Change import**:
|
|
266
|
+
```python
|
|
267
|
+
# from qh import mk_http_service_app
|
|
268
|
+
from py2mcp import mk_mcp_server
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
2. **Change function name**:
|
|
272
|
+
```python
|
|
273
|
+
# app = mk_http_service_app([...])
|
|
274
|
+
mcp = mk_mcp_server([...])
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
3. **Update transformations** (if used):
|
|
278
|
+
```python
|
|
279
|
+
# from qh.trans import mk_json_handler_from_name_mapping
|
|
280
|
+
from py2mcp import mk_input_trans
|
|
281
|
+
|
|
282
|
+
# input_trans = mk_json_handler_from_name_mapping({...})
|
|
283
|
+
input_trans = mk_input_trans({...})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
4. **Change run call**:
|
|
287
|
+
```python
|
|
288
|
+
# app.run(port=8080)
|
|
289
|
+
mcp.run() # or mcp.run(transport='http', port=8000)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
That's it! Your functions stay exactly the same.
|
|
293
|
+
|
|
294
|
+
## When to Use Which?
|
|
295
|
+
|
|
296
|
+
### Use qh (py2http) when:
|
|
297
|
+
- You need HTTP/REST endpoints
|
|
298
|
+
- Building a web API
|
|
299
|
+
- Want browser/curl access
|
|
300
|
+
- Integrating with HTTP clients
|
|
301
|
+
|
|
302
|
+
### Use py2mcp when:
|
|
303
|
+
- Building AI agent tools
|
|
304
|
+
- Integrating with Claude/Cursor
|
|
305
|
+
- Want LLM-friendly interfaces
|
|
306
|
+
- Following MCP standard
|
|
307
|
+
|
|
308
|
+
### Use both when:
|
|
309
|
+
- You want both HTTP and MCP access
|
|
310
|
+
- Maximum flexibility
|
|
311
|
+
- Different client types
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from qh import mk_http_service_app
|
|
315
|
+
from py2mcp import mk_mcp_server
|
|
316
|
+
|
|
317
|
+
# Same functions, two servers!
|
|
318
|
+
funcs = [add, multiply, greet]
|
|
319
|
+
|
|
320
|
+
http_app = mk_http_service_app(funcs)
|
|
321
|
+
mcp_server = mk_mcp_server(funcs)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Design Philosophy
|
|
325
|
+
|
|
326
|
+
Both packages follow the same principles:
|
|
327
|
+
|
|
328
|
+
1. **Simplicity**: Minimal API surface
|
|
329
|
+
2. **Convention over configuration**: Sensible defaults
|
|
330
|
+
3. **Don't reinvent**: Build on proven frameworks
|
|
331
|
+
4. **Pythonic**: Feels natural to Python developers
|
|
332
|
+
5. **Functional**: Prefer functions over classes
|
|
333
|
+
6. **SSOT**: Single source of truth (the functions)
|
|
334
|
+
7. **Open/closed**: Easy to extend, works out of the box
|
|
335
|
+
|
|
336
|
+
## Conclusion
|
|
337
|
+
|
|
338
|
+
`py2mcp` is essentially "qh for MCP" - the same clean, simple pattern applied to a different protocol. If you liked how qh worked, you'll feel right at home with py2mcp.
|
|
339
|
+
|
|
340
|
+
The key insight: **Functions are the universal interface.** Whether exposing them via HTTP or MCP, the pattern is the same:
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
functions → wrapper → server → run
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Simple, clean, Pythonic. ✨
|