restmcp 0.1.2__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.
Files changed (48) hide show
  1. restmcp-0.1.2/.github/workflows/publish.yml +53 -0
  2. restmcp-0.1.2/.gitignore +51 -0
  3. restmcp-0.1.2/LICENSE +21 -0
  4. restmcp-0.1.2/PKG-INFO +406 -0
  5. restmcp-0.1.2/README.md +372 -0
  6. restmcp-0.1.2/conftest.py +4 -0
  7. restmcp-0.1.2/example/datasource/__init__.py +0 -0
  8. restmcp-0.1.2/example/datasource/posts_async.py +21 -0
  9. restmcp-0.1.2/example/datasource/posts_sync.py +19 -0
  10. restmcp-0.1.2/example/main.py +7 -0
  11. restmcp-0.1.2/example/models/__init__.py +0 -0
  12. restmcp-0.1.2/example/models/post.py +8 -0
  13. restmcp-0.1.2/example/pyproject.toml +14 -0
  14. restmcp-0.1.2/example/repositories/__init__.py +0 -0
  15. restmcp-0.1.2/example/repositories/posts.py +17 -0
  16. restmcp-0.1.2/example/repositories/posts_async.py +17 -0
  17. restmcp-0.1.2/example/services/__init__.py +0 -0
  18. restmcp-0.1.2/example/services/posts.py +25 -0
  19. restmcp-0.1.2/example/test_example.py +213 -0
  20. restmcp-0.1.2/example/tools/__init__.py +0 -0
  21. restmcp-0.1.2/example/urls/__init__.py +7 -0
  22. restmcp-0.1.2/example/urls/posts.py +76 -0
  23. restmcp-0.1.2/pyproject.toml +48 -0
  24. restmcp-0.1.2/restmcp/__init__.py +22 -0
  25. restmcp-0.1.2/restmcp/cli/__init__.py +11 -0
  26. restmcp-0.1.2/restmcp/cli/new.py +65 -0
  27. restmcp-0.1.2/restmcp/datasource.py +18 -0
  28. restmcp-0.1.2/restmcp/endpoint.py +140 -0
  29. restmcp-0.1.2/restmcp/entity.py +13 -0
  30. restmcp-0.1.2/restmcp/exceptions.py +17 -0
  31. restmcp-0.1.2/restmcp/logging.py +35 -0
  32. restmcp-0.1.2/restmcp/mcp.py +66 -0
  33. restmcp-0.1.2/restmcp/repository.py +35 -0
  34. restmcp-0.1.2/restmcp/rest.py +63 -0
  35. restmcp-0.1.2/restmcp/server.py +51 -0
  36. restmcp-0.1.2/restmcp/service.py +35 -0
  37. restmcp-0.1.2/restmcp/types.py +22 -0
  38. restmcp-0.1.2/tests/conftest.py +8 -0
  39. restmcp-0.1.2/tests/test_cli.py +54 -0
  40. restmcp-0.1.2/tests/test_datasource.py +28 -0
  41. restmcp-0.1.2/tests/test_endpoint.py +380 -0
  42. restmcp-0.1.2/tests/test_entity.py +36 -0
  43. restmcp-0.1.2/tests/test_exceptions.py +36 -0
  44. restmcp-0.1.2/tests/test_logger.py +67 -0
  45. restmcp-0.1.2/tests/test_mcp.py +168 -0
  46. restmcp-0.1.2/tests/test_repository.py +86 -0
  47. restmcp-0.1.2/tests/test_server.py +128 -0
  48. restmcp-0.1.2/tests/test_service.py +97 -0
@@ -0,0 +1,53 @@
1
+ name: Test & Publish
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with:
14
+ python-version: "3.13"
15
+ - run: pip install -e ".[dev]"
16
+ - run: pytest --cov=restmcp --cov-report=xml
17
+ - uses: codecov/codecov-action@v4
18
+ with:
19
+ token: ${{ secrets.CODECOV_TOKEN }}
20
+ files: coverage.xml
21
+
22
+ publish-testpypi:
23
+ needs: test
24
+ runs-on: ubuntu-latest
25
+ environment: testpypi
26
+ permissions:
27
+ id-token: write
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: "3.13"
33
+ - run: pip install build
34
+ - run: python -m build
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
36
+ with:
37
+ repository-url: https://test.pypi.org/legacy/
38
+ skip-existing: true
39
+
40
+ publish-pypi:
41
+ needs: publish-testpypi
42
+ runs-on: ubuntu-latest
43
+ environment: pypi
44
+ permissions:
45
+ id-token: write
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+ - uses: actions/setup-python@v5
49
+ with:
50
+ python-version: "3.13"
51
+ - run: pip install build
52
+ - run: python -m build
53
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,51 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ wheels/
12
+ *.whl
13
+ MANIFEST
14
+
15
+ # Virtual environments
16
+ .venv/
17
+ venv/
18
+ env/
19
+ .env/
20
+
21
+ # Environment variables
22
+ .env
23
+ .env.*
24
+ !.env.example
25
+
26
+ # pytest / coverage
27
+ .pytest_cache/
28
+ .coverage
29
+ .coverage.*
30
+ htmlcov/
31
+ coverage.xml
32
+
33
+ # Type checkers
34
+ .mypy_cache/
35
+ .dmypy.json
36
+ .pytype/
37
+ .pyre/
38
+
39
+ # IDEs
40
+ .idea/
41
+ .vscode/
42
+ *.swp
43
+ *.swo
44
+ *~
45
+ .DS_Store
46
+
47
+ # hatch / build
48
+ .hatch/
49
+
50
+ # internal dev docs
51
+ docs/superpowers/
restmcp-0.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JorgeHSantana
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.
restmcp-0.1.2/PKG-INFO ADDED
@@ -0,0 +1,406 @@
1
+ Metadata-Version: 2.4
2
+ Name: restmcp
3
+ Version: 0.1.2
4
+ Summary: Python framework for building MCP servers with a layered architecture and REST compatibility
5
+ Project-URL: Homepage, https://github.com/JorgeHSantana/restmcp
6
+ Project-URL: Repository, https://github.com/JorgeHSantana/restmcp
7
+ Project-URL: Bug Tracker, https://github.com/JorgeHSantana/restmcp/issues
8
+ Author-email: Jorge Henrique Moreira Santana <jorge.henrique.moreira.santana@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,async,fastapi,framework,llm,mcp,rest,server
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
21
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: click>=8.0
24
+ Requires-Dist: fastapi>=0.100
25
+ Requires-Dist: fastmcp>=2.0
26
+ Requires-Dist: pydantic>=2.0
27
+ Requires-Dist: uvicorn[standard]>=0.20
28
+ Provides-Extra: dev
29
+ Requires-Dist: httpx>=0.24; extra == 'dev'
30
+ Requires-Dist: pytest; extra == 'dev'
31
+ Requires-Dist: pytest-cov; extra == 'dev'
32
+ Requires-Dist: requests>=2.28; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # restmcp
36
+
37
+ [![codecov](https://codecov.io/gh/JorgeHSantana/restmcp/branch/main/graph/badge.svg)](https://codecov.io/gh/JorgeHSantana/restmcp)
38
+
39
+ > One framework. MCP tools and REST endpoints, auto-registered.
40
+
41
+ Python framework for building **MCP servers** with a layered architecture and REST compatibility.
42
+ Annotated classes become MCP tools and HTTP endpoints: auto-registered, dependency-injected, sync/async agnostic.
43
+
44
+ ---
45
+
46
+ ## Architecture
47
+
48
+ ```mermaid
49
+ graph LR
50
+ LLM["🤖 LLM / Client"] -->|"HTTP or MCP"| EP["Endpoint"]
51
+ EP --> SV["Service"]
52
+ SV --> RP["Repository"]
53
+ RP --> DS["DataSource"]
54
+ DS --> EX[("External\nAPI / DB")]
55
+
56
+ style EP fill:#4f46e5,color:#fff,stroke:none
57
+ style SV fill:#7c3aed,color:#fff,stroke:none
58
+ style RP fill:#9333ea,color:#fff,stroke:none
59
+ style DS fill:#a855f7,color:#fff,stroke:none
60
+ ```
61
+
62
+ Each layer knows only the layer directly below it. Every class name is suffix-enforced at import time: a typo raises `TypeError` before the server starts.
63
+
64
+ ---
65
+
66
+ ## Installation
67
+
68
+ ```bash
69
+ pip install restmcp
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Quick start
75
+
76
+ ```bash
77
+ restmcp new my-server
78
+ cd my-server
79
+ pip install -e .
80
+ python main.py
81
+ ```
82
+
83
+ Generated structure:
84
+
85
+ ```
86
+ my-server/
87
+ ├── datasource/ # external connections (APIs, databases)
88
+ ├── models/ # domain entities (Pydantic)
89
+ ├── repositories/ # data access layer
90
+ ├── services/ # business logic
91
+ ├── tools/ # internal utilities
92
+ ├── urls/ # endpoint definitions (auto-discovery)
93
+ ├── main.py
94
+ └── pyproject.toml
95
+ ```
96
+
97
+ ---
98
+
99
+ ## How it works
100
+
101
+ ```mermaid
102
+ sequenceDiagram
103
+ participant C as Client / LLM
104
+ participant E as Endpoint
105
+ participant S as Service
106
+ participant R as Repository
107
+ participant D as DataSource
108
+
109
+ C->>E: POST /api/get-product {"product_id": "1"}
110
+ E->>S: service.execute(product_id="1")
111
+ S->>R: repo.get(product_id="1")
112
+ R->>D: data_source.fetch("1")
113
+ D-->>R: raw dict
114
+ R-->>S: ProductEntity
115
+ S-->>E: result dict
116
+ E-->>C: {"tool": "get_product", "result": {...}, "success": true}
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Base classes
122
+
123
+ ### `DataSource`
124
+
125
+ Abstracts the connection to an external data source (REST API, database, file).
126
+ **Rule:** class name must end with `DataSource`.
127
+
128
+ ```python
129
+ import httpx
130
+ from restmcp import DataSource
131
+
132
+ class ProductApiDataSource(DataSource):
133
+ base_url = "https://api.example.com"
134
+
135
+ async def fetch(self, product_id: str) -> dict:
136
+ async with httpx.AsyncClient() as client:
137
+ r = await client.get(f"{self.base_url}/products/{product_id}")
138
+ r.raise_for_status()
139
+ return r.json()
140
+ ```
141
+
142
+ ---
143
+
144
+ ### `Entity`
145
+
146
+ Structured domain data backed by Pydantic. Automatic type validation.
147
+ **Rule:** class name must end with `Entity`.
148
+
149
+ ```python
150
+ from restmcp import Entity
151
+
152
+ class ProductEntity(Entity):
153
+ id: str
154
+ name: str
155
+ price: float
156
+ ```
157
+
158
+ ---
159
+
160
+ ### `Repository`
161
+
162
+ Fetches data via a `DataSource` and returns `Entity` objects. One source, one data type.
163
+ **Rules:** name ends with `Repository`; must declare `data_source` as class attribute; must implement `get()`.
164
+
165
+ ```python
166
+ from restmcp import Repository
167
+ from datasource.product_api import ProductApiDataSource
168
+ from models.product import ProductEntity
169
+
170
+ class ProductRepository(Repository):
171
+ data_source = ProductApiDataSource()
172
+
173
+ async def get(self, product_id: str) -> ProductEntity:
174
+ raw = await self.data_source.fetch(product_id)
175
+ return ProductEntity(**raw)
176
+ ```
177
+
178
+ **Dependency injection:**
179
+
180
+ ```python
181
+ repo = ProductRepository() # uses real DataSource
182
+ repo = ProductRepository(data_source=MockDataSource()) # injects mock for tests
183
+ ```
184
+
185
+ `Repository.__init__` uses `copy.copy()` of the class attribute: instances are always isolated.
186
+
187
+ ---
188
+
189
+ ### `Service`
190
+
191
+ Orchestrates business logic. Where joins, transformations, and multi-source rules live.
192
+ **Rules:** name ends with `Service`; must declare at least one `Repository` as class attribute.
193
+
194
+ ```python
195
+ from restmcp import Service
196
+ from repositories.product import ProductRepository
197
+
198
+ class GetProductService(Service):
199
+ repo = ProductRepository()
200
+
201
+ async def execute(self, product_id: str) -> dict:
202
+ product = await self.repo.get(product_id=product_id)
203
+ return product.model_dump()
204
+ ```
205
+
206
+ **Dependency injection:**
207
+
208
+ ```python
209
+ svc = GetProductService() # production
210
+ svc = GetProductService(repo=MockRepository()) # test
211
+ ```
212
+
213
+ Repository class attributes are auto-discovered via MRO and isolated per instance.
214
+
215
+ ---
216
+
217
+ ### `Endpoint`
218
+
219
+ HTTP + MCP route. **Auto-registers on class definition**: no manual wiring needed.
220
+ **Rules:** name ends with `Endpoint`; must declare `mcp_definition`, `url`, `method`, and `callback`.
221
+
222
+ ```python
223
+ from restmcp import Endpoint
224
+ from services.product import GetProductService
225
+
226
+ class GetProductEndpoint(Endpoint):
227
+ mcp_definition = {
228
+ "name": "get_product",
229
+ "description": "Returns a product by ID",
230
+ "parameters": {
231
+ "properties": {
232
+ "product_id": {"type": "string", "description": "Product ID"},
233
+ },
234
+ },
235
+ }
236
+ url = "/api/get-product"
237
+ method = "POST"
238
+
239
+ async def callback(self, product_id: str) -> dict:
240
+ return await GetProductService().execute(product_id)
241
+ ```
242
+
243
+ Defining the class is enough. The route is registered on the `Server` singleton the moment Python processes the class body.
244
+
245
+ **Disabling an endpoint:**
246
+
247
+ ```python
248
+ class GetProductEndpoint(Endpoint):
249
+ disabled = True # skips auto-registration; can still be instantiated manually
250
+ ...
251
+ ```
252
+
253
+ **Abstract base classes** (missing any required attribute) are never auto-registered:
254
+
255
+ ```python
256
+ class BaseAuthEndpoint(Endpoint):
257
+ method = "POST"
258
+ def callback(self, **kwargs): ...
259
+ # ↑ not registered: url and mcp_definition are missing
260
+
261
+ class GetUserEndpoint(BaseAuthEndpoint):
262
+ mcp_definition = { ... }
263
+ url = "/api/get-user"
264
+ # ↑ registered automatically: all required attributes present
265
+ ```
266
+
267
+ **Sync and async callbacks** are both supported: restmcp detects and handles either:
268
+
269
+ ```python
270
+ # sync: runs in a thread pool, does not block the event loop
271
+ def callback(self, product_id: str) -> dict:
272
+ return requests.get(f"https://api.example.com/products/{product_id}").json()
273
+
274
+ # async: awaited directly; use asyncio.gather for parallel I/O
275
+ async def callback(self, product_id: str) -> dict:
276
+ async with httpx.AsyncClient() as client:
277
+ r = await client.get(f"https://api.example.com/products/{product_id}")
278
+ return r.json()
279
+ ```
280
+
281
+ **Response format:**
282
+
283
+ ```json
284
+ { "tool": "get_product", "result": { ... }, "success": true }
285
+ ```
286
+
287
+ ```json
288
+ { "tool": "get_product", "error": "not found", "error_type": "NotFoundError", "success": false }
289
+ ```
290
+
291
+ ---
292
+
293
+ ### `Server`
294
+
295
+ Singleton with dual-mode: HTTP via FastAPI/uvicorn or MCP protocol via FastMCP.
296
+
297
+ ```python
298
+ from restmcp import Server
299
+ import urls # triggers auto-discovery of all endpoint modules
300
+
301
+ server = Server.get_instance()
302
+
303
+ if __name__ == "__main__":
304
+ server.start(host="0.0.0.0", port=5000)
305
+ ```
306
+
307
+ ```python
308
+ # MCP mode
309
+ mcp = server.get_mcp()
310
+ ```
311
+
312
+ **Built-in routes:**
313
+
314
+ | Route | Method | Auth required |
315
+ |-------|--------|---------------|
316
+ | `/health` | GET | No |
317
+ | `/mcp/tools` | GET | No |
318
+ | _your endpoints_ | POST | Yes (if `AUTH_API_KEY` is set) |
319
+
320
+ ---
321
+
322
+ ## Exceptions
323
+
324
+ Raised inside `callback`: caught by `Endpoint` and converted to HTTP responses automatically.
325
+
326
+ ```python
327
+ from restmcp import ValidationError, NotFoundError
328
+
329
+ raise ValidationError("product_id is required") # → HTTP 400
330
+ raise NotFoundError("Product not found") # → HTTP 404
331
+ ```
332
+
333
+ ```mermaid
334
+ graph TD
335
+ PythiaException --> ValidationError["ValidationError (400)"]
336
+ PythiaException --> NotFoundError["NotFoundError (404)"]
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Testing with injection
342
+
343
+ ```python
344
+ from restmcp import DataSource
345
+ from repositories.product import ProductRepository
346
+ from services.product import GetProductService
347
+
348
+ class FakeProductApiDataSource(DataSource):
349
+ async def fetch(self, product_id: str) -> dict:
350
+ return {"id": product_id, "name": "Test Widget", "price": 1.99}
351
+
352
+ def test_get_product():
353
+ svc = GetProductService(repo=ProductRepository(data_source=FakeProductApiDataSource()))
354
+ result = svc.execute(product_id="1")
355
+ assert result["name"] == "Test Widget"
356
+ ```
357
+
358
+ ---
359
+
360
+ ## Environment variables
361
+
362
+ | Variable | Default | Description |
363
+ |----------|---------|-------------|
364
+ | `AUTH_API_KEY` | _(disabled)_ | Bearer token. Multiple keys supported comma-separated. |
365
+ | `CORS_ORIGINS` | `*` | Allowed origins. Multiple values supported comma-separated. |
366
+ | `LOG_LEVEL` | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`. |
367
+
368
+ ---
369
+
370
+ ## Naming conventions
371
+
372
+ All base classes enforce a suffix. Violating it raises `TypeError` at import time: before the server starts.
373
+
374
+ | Base class | Required suffix | Example |
375
+ |------------|----------------|---------|
376
+ | `DataSource` | `*DataSource` | `ProductApiDataSource` |
377
+ | `Entity` | `*Entity` | `ProductEntity` |
378
+ | `Repository` | `*Repository` | `ProductRepository` |
379
+ | `Service` | `*Service` | `GetProductService` |
380
+ | `Endpoint` | `*Endpoint` | `GetProductEndpoint` |
381
+
382
+ ---
383
+
384
+ ## Dependencies
385
+
386
+ ```
387
+ fastapi >= 0.100
388
+ uvicorn >= 0.20
389
+ fastmcp >= 2.0
390
+ pydantic >= 2.0
391
+ click >= 8.0
392
+ ```
393
+
394
+ ---
395
+
396
+ ## Author
397
+
398
+ **Jorge Henrique Moreira Santana**
399
+ Electrical Engineer, Postgraduate in Artificial Intelligence
400
+ [LinkedIn](https://www.linkedin.com/in/jorge-santana-b246874a/) · jorge.henrique.moreira.santana@gmail.com
401
+
402
+ ---
403
+
404
+ ## License
405
+
406
+ [MIT](LICENSE)