restmcp 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.
- pythia/__init__.py +22 -0
- pythia/cli/__init__.py +11 -0
- pythia/cli/new.py +65 -0
- pythia/datasource.py +18 -0
- pythia/endpoint.py +140 -0
- pythia/entity.py +13 -0
- pythia/exceptions.py +17 -0
- pythia/logging.py +35 -0
- pythia/mcp.py +66 -0
- pythia/repository.py +35 -0
- pythia/rest.py +63 -0
- pythia/server.py +51 -0
- pythia/service.py +35 -0
- pythia/types.py +22 -0
- restmcp-0.1.0.dist-info/METADATA +403 -0
- restmcp-0.1.0.dist-info/RECORD +19 -0
- restmcp-0.1.0.dist-info/WHEEL +4 -0
- restmcp-0.1.0.dist-info/entry_points.txt +2 -0
- restmcp-0.1.0.dist-info/licenses/LICENSE +21 -0
pythia/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pythia.datasource import DataSource
|
|
2
|
+
from pythia.entity import Entity
|
|
3
|
+
from pythia.repository import Repository
|
|
4
|
+
from pythia.service import Service
|
|
5
|
+
from pythia.endpoint import Endpoint
|
|
6
|
+
from pythia.server import Server
|
|
7
|
+
from pythia.logging import Logger
|
|
8
|
+
from pythia.exceptions import ValidationError, NotFoundError
|
|
9
|
+
from pythia.types import McpDefinition
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DataSource",
|
|
13
|
+
"Entity",
|
|
14
|
+
"Repository",
|
|
15
|
+
"Service",
|
|
16
|
+
"Endpoint",
|
|
17
|
+
"Server",
|
|
18
|
+
"Logger",
|
|
19
|
+
"ValidationError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"McpDefinition",
|
|
22
|
+
]
|
pythia/cli/__init__.py
ADDED
pythia/cli/new.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import click
|
|
3
|
+
|
|
4
|
+
TEMPLATE_DIRS = ["datasource", "models", "repositories", "services", "tools", "urls"]
|
|
5
|
+
|
|
6
|
+
MAIN_PY = """\
|
|
7
|
+
from pythia import Server
|
|
8
|
+
import urls # auto-discovery
|
|
9
|
+
|
|
10
|
+
server = Server.get_instance()
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
server.start()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
URLS_INIT = """\
|
|
17
|
+
import importlib
|
|
18
|
+
import pkgutil
|
|
19
|
+
|
|
20
|
+
for _info in pkgutil.iter_modules(__path__):
|
|
21
|
+
importlib.import_module(f"{__name__}.{_info.name}")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
PYPROJECT = """\
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[project]
|
|
30
|
+
name = "{name}"
|
|
31
|
+
version = "0.1.0"
|
|
32
|
+
requires-python = ">=3.11"
|
|
33
|
+
dependencies = [
|
|
34
|
+
"pythia",
|
|
35
|
+
]
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@click.command()
|
|
40
|
+
@click.argument("name")
|
|
41
|
+
def new_command(name: str):
|
|
42
|
+
"""Create a new MCP server with the pythia structure."""
|
|
43
|
+
base = os.path.join(os.getcwd(), name)
|
|
44
|
+
|
|
45
|
+
if os.path.exists(base):
|
|
46
|
+
click.echo(f"Error: directory '{name}' already exists.", err=True)
|
|
47
|
+
raise SystemExit(1)
|
|
48
|
+
|
|
49
|
+
os.makedirs(base)
|
|
50
|
+
|
|
51
|
+
for d in TEMPLATE_DIRS:
|
|
52
|
+
os.makedirs(os.path.join(base, d))
|
|
53
|
+
open(os.path.join(base, d, "__init__.py"), "w").close()
|
|
54
|
+
|
|
55
|
+
with open(os.path.join(base, "urls", "__init__.py"), "w") as f:
|
|
56
|
+
f.write(URLS_INIT)
|
|
57
|
+
|
|
58
|
+
with open(os.path.join(base, "main.py"), "w") as f:
|
|
59
|
+
f.write(MAIN_PY)
|
|
60
|
+
|
|
61
|
+
with open(os.path.join(base, "pyproject.toml"), "w") as f:
|
|
62
|
+
f.write(PYPROJECT.format(name=name))
|
|
63
|
+
|
|
64
|
+
click.echo(f"Project '{name}' created successfully.")
|
|
65
|
+
click.echo(f" cd {name} && pip install -e . && python main.py")
|
pythia/datasource.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DataSource(ABC):
|
|
5
|
+
"""External data provider (HTTP, DB, file). Subclass names must end with 'DataSource'."""
|
|
6
|
+
|
|
7
|
+
def __init_subclass__(cls, **kwargs):
|
|
8
|
+
super().__init_subclass__(**kwargs)
|
|
9
|
+
if not cls.__name__.endswith("DataSource"):
|
|
10
|
+
raise TypeError(
|
|
11
|
+
f"DataSource subclasses must end with 'DataSource' "
|
|
12
|
+
f"(got: '{cls.__name__}'). Rename to '{cls.__name__}DataSource'."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def __new__(cls, *args, **kwargs):
|
|
16
|
+
if cls is DataSource:
|
|
17
|
+
raise TypeError("DataSource is abstract and cannot be instantiated directly.")
|
|
18
|
+
return super().__new__(cls)
|
pythia/endpoint.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
from abc import ABC
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends, Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
|
|
8
|
+
from pythia.exceptions import PythiaException, ValidationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _validate_mcp_definition(cls_name: str, mcp_def: object) -> None:
|
|
12
|
+
if not isinstance(mcp_def, dict):
|
|
13
|
+
raise TypeError(
|
|
14
|
+
f"{cls_name}: mcp_definition must be a dict, got {type(mcp_def).__name__}"
|
|
15
|
+
)
|
|
16
|
+
for key in ("name", "description"):
|
|
17
|
+
val = mcp_def.get(key)
|
|
18
|
+
if not isinstance(val, str) or not val.strip():
|
|
19
|
+
raise TypeError(
|
|
20
|
+
f"{cls_name}: mcp_definition['{key}'] must be a non-empty string"
|
|
21
|
+
)
|
|
22
|
+
params = mcp_def.get("parameters")
|
|
23
|
+
if params is not None:
|
|
24
|
+
if not isinstance(params, dict):
|
|
25
|
+
raise TypeError(
|
|
26
|
+
f"{cls_name}: mcp_definition['parameters'] must be a dict"
|
|
27
|
+
)
|
|
28
|
+
props = params.get("properties")
|
|
29
|
+
if props is not None and not isinstance(props, dict):
|
|
30
|
+
raise TypeError(
|
|
31
|
+
f"{cls_name}: mcp_definition['parameters']['properties'] must be a dict"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Endpoint(ABC):
|
|
36
|
+
"""HTTP + MCP endpoint. Subclasses auto-register on class definition when url, method, mcp_definition, and callback are all set."""
|
|
37
|
+
|
|
38
|
+
disabled: bool = False
|
|
39
|
+
|
|
40
|
+
def __init_subclass__(cls, **kwargs):
|
|
41
|
+
super().__init_subclass__(**kwargs)
|
|
42
|
+
if not cls.__name__.endswith("Endpoint"):
|
|
43
|
+
raise TypeError(
|
|
44
|
+
f"Endpoint subclasses must end with 'Endpoint' "
|
|
45
|
+
f"(got: '{cls.__name__}'). Rename to '{cls.__name__}Endpoint'."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if getattr(cls, "disabled", False):
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
_required = ("url", "method", "mcp_definition", "callback")
|
|
52
|
+
if all(vars(cls).get(attr) for attr in _required):
|
|
53
|
+
_validate_mcp_definition(cls.__name__, vars(cls)["mcp_definition"])
|
|
54
|
+
try:
|
|
55
|
+
cls()
|
|
56
|
+
except TypeError as e:
|
|
57
|
+
raise TypeError(
|
|
58
|
+
f"{cls.__name__}: auto-registration failed. "
|
|
59
|
+
f"Endpoint subclasses must not define __init__ with parameters — "
|
|
60
|
+
f"use Service/Repository for dependencies. Original error: {e}"
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
async def _callback(self, request: Request):
|
|
64
|
+
try:
|
|
65
|
+
try:
|
|
66
|
+
data = await request.json()
|
|
67
|
+
except Exception:
|
|
68
|
+
data = {}
|
|
69
|
+
data = data or {}
|
|
70
|
+
|
|
71
|
+
valid_params = self.mcp_definition.get("parameters", {}).get("properties", {})
|
|
72
|
+
parameters = {}
|
|
73
|
+
|
|
74
|
+
for key, value in data.items():
|
|
75
|
+
if key not in valid_params:
|
|
76
|
+
raise ValidationError(f"Invalid parameter: {key}")
|
|
77
|
+
parameters[key] = value
|
|
78
|
+
|
|
79
|
+
if inspect.iscoroutinefunction(self.callback):
|
|
80
|
+
result = await self.callback(**parameters)
|
|
81
|
+
else:
|
|
82
|
+
loop = asyncio.get_running_loop()
|
|
83
|
+
result = await loop.run_in_executor(
|
|
84
|
+
None, lambda: self.callback(**parameters)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return JSONResponse({
|
|
88
|
+
"tool": self.mcp_definition["name"],
|
|
89
|
+
"result": result,
|
|
90
|
+
"success": True,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
except PythiaException as e:
|
|
94
|
+
return JSONResponse({
|
|
95
|
+
"error": e.message,
|
|
96
|
+
"tool": self.mcp_definition["name"],
|
|
97
|
+
"success": False,
|
|
98
|
+
"error_type": e.__class__.__name__,
|
|
99
|
+
}, status_code=e.status_code)
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
return JSONResponse({
|
|
103
|
+
"error": str(e),
|
|
104
|
+
"tool": self.mcp_definition["name"],
|
|
105
|
+
"success": False,
|
|
106
|
+
"error_type": "InternalServerError",
|
|
107
|
+
}, status_code=500)
|
|
108
|
+
|
|
109
|
+
def __init__(self):
|
|
110
|
+
from pythia.server import Server
|
|
111
|
+
from pythia.rest import _auth_dependency
|
|
112
|
+
|
|
113
|
+
self.mcp_definition = getattr(self, "mcp_definition", None)
|
|
114
|
+
if not self.mcp_definition:
|
|
115
|
+
raise ValueError(f"{self.__class__.__name__}: mcp_definition is required")
|
|
116
|
+
|
|
117
|
+
self.method = getattr(self, "method", None)
|
|
118
|
+
if not self.method:
|
|
119
|
+
raise ValueError(f"{self.__class__.__name__}: method is required")
|
|
120
|
+
|
|
121
|
+
self.url = getattr(self, "url", None)
|
|
122
|
+
if not self.url:
|
|
123
|
+
raise ValueError(f"{self.__class__.__name__}: url is required")
|
|
124
|
+
|
|
125
|
+
if not getattr(self, "callback", None):
|
|
126
|
+
raise ValueError(f"{self.__class__.__name__}: callback is required")
|
|
127
|
+
|
|
128
|
+
endpoint_self = self
|
|
129
|
+
|
|
130
|
+
async def route_handler(request: Request):
|
|
131
|
+
return await endpoint_self._callback(request)
|
|
132
|
+
|
|
133
|
+
server = Server.get_instance()
|
|
134
|
+
server.app.add_api_route(
|
|
135
|
+
self.url,
|
|
136
|
+
route_handler,
|
|
137
|
+
methods=[self.method],
|
|
138
|
+
dependencies=[Depends(_auth_dependency)],
|
|
139
|
+
)
|
|
140
|
+
server.register_url_handler(self)
|
pythia/entity.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Entity(BaseModel):
|
|
5
|
+
"""Pydantic model for data returned by a DataSource. Subclass names must end with 'Entity'."""
|
|
6
|
+
|
|
7
|
+
def __init_subclass__(cls, **kwargs):
|
|
8
|
+
super().__init_subclass__(**kwargs)
|
|
9
|
+
if not cls.__name__.endswith("Entity"):
|
|
10
|
+
raise TypeError(
|
|
11
|
+
f"Entity subclasses must end with 'Entity' "
|
|
12
|
+
f"(got: '{cls.__name__}'). Rename to '{cls.__name__}Entity'."
|
|
13
|
+
)
|
pythia/exceptions.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class PythiaException(Exception):
|
|
2
|
+
"""Base exception for pythia. Carries an HTTP status code alongside the message."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, message: str, status_code: int = 500):
|
|
5
|
+
self.message = message
|
|
6
|
+
self.status_code = status_code
|
|
7
|
+
super().__init__(message)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ValidationError(PythiaException):
|
|
11
|
+
def __init__(self, message: str):
|
|
12
|
+
super().__init__(message, status_code=400)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NotFoundError(PythiaException):
|
|
16
|
+
def __init__(self, message: str):
|
|
17
|
+
super().__init__(message, status_code=404)
|
pythia/logging.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Logger:
|
|
6
|
+
"""Thin wrapper over Python's logging module. Log level configurable via LOG_LEVEL env var (default: INFO)."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, name: str):
|
|
9
|
+
level_name = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
10
|
+
level = getattr(logging, level_name, logging.INFO)
|
|
11
|
+
|
|
12
|
+
self._logger = logging.getLogger(name)
|
|
13
|
+
self._logger.setLevel(level)
|
|
14
|
+
|
|
15
|
+
if not self._logger.handlers:
|
|
16
|
+
handler = logging.StreamHandler()
|
|
17
|
+
handler.setLevel(level)
|
|
18
|
+
formatter = logging.Formatter(
|
|
19
|
+
"[%(asctime)s] %(levelname)s %(name)s — %(message)s",
|
|
20
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
21
|
+
)
|
|
22
|
+
handler.setFormatter(formatter)
|
|
23
|
+
self._logger.addHandler(handler)
|
|
24
|
+
|
|
25
|
+
def info(self, msg: str, *args, **kwargs):
|
|
26
|
+
self._logger.info(msg, *args, **kwargs)
|
|
27
|
+
|
|
28
|
+
def warning(self, msg: str, *args, **kwargs):
|
|
29
|
+
self._logger.warning(msg, *args, **kwargs)
|
|
30
|
+
|
|
31
|
+
def error(self, msg: str, *args, **kwargs):
|
|
32
|
+
self._logger.error(msg, *args, **kwargs)
|
|
33
|
+
|
|
34
|
+
def debug(self, msg: str, *args, **kwargs):
|
|
35
|
+
self._logger.debug(msg, *args, **kwargs)
|
pythia/mcp.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
_JSON_TO_PYTHON = {
|
|
5
|
+
"string": str,
|
|
6
|
+
"integer": int,
|
|
7
|
+
"number": float,
|
|
8
|
+
"boolean": bool,
|
|
9
|
+
"object": dict,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class McpApp:
|
|
14
|
+
"""Builds a FastMCP instance from registered url_handlers, mapping mcp_definition to typed pydantic tool wrappers."""
|
|
15
|
+
|
|
16
|
+
def build(self, url_handlers: List[Any]):
|
|
17
|
+
from fastmcp import FastMCP
|
|
18
|
+
|
|
19
|
+
mcp = FastMCP("pythia")
|
|
20
|
+
for handler in url_handlers:
|
|
21
|
+
self._register_tool(mcp, handler)
|
|
22
|
+
return mcp
|
|
23
|
+
|
|
24
|
+
def _register_tool(self, mcp: Any, handler: Any):
|
|
25
|
+
from pydantic import Field, create_model
|
|
26
|
+
import inspect
|
|
27
|
+
|
|
28
|
+
def_dict = handler.mcp_definition
|
|
29
|
+
name = def_dict["name"]
|
|
30
|
+
description = def_dict["description"]
|
|
31
|
+
properties = def_dict.get("parameters", {}).get("properties", {})
|
|
32
|
+
|
|
33
|
+
pydantic_fields = {}
|
|
34
|
+
for prop_name, prop_data in properties.items():
|
|
35
|
+
ptype = prop_data.get("type")
|
|
36
|
+
if ptype == "array":
|
|
37
|
+
item_ptype = prop_data.get("items", {}).get("type", "string")
|
|
38
|
+
item_type = _JSON_TO_PYTHON.get(item_ptype, str)
|
|
39
|
+
py_type = List[item_type]
|
|
40
|
+
elif ptype == "object":
|
|
41
|
+
py_type = Dict[str, Any]
|
|
42
|
+
else:
|
|
43
|
+
py_type = _JSON_TO_PYTHON.get(ptype, str)
|
|
44
|
+
|
|
45
|
+
default_val = prop_data.get("default", ...)
|
|
46
|
+
if default_val is None:
|
|
47
|
+
py_type = Optional[py_type]
|
|
48
|
+
|
|
49
|
+
pydantic_fields[prop_name] = (
|
|
50
|
+
py_type,
|
|
51
|
+
Field(default=default_val, description=prop_data.get("description", "")),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
ModelClass = create_model(f"{name}_args", **pydantic_fields)
|
|
55
|
+
|
|
56
|
+
def tool_wrapper(args: ModelClass) -> dict:
|
|
57
|
+
return handler.callback(**args.model_dump())
|
|
58
|
+
|
|
59
|
+
tool_wrapper.__name__ = name
|
|
60
|
+
tool_wrapper.__doc__ = description
|
|
61
|
+
sig = inspect.signature(tool_wrapper)
|
|
62
|
+
new_param = inspect.Parameter(
|
|
63
|
+
"args", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=ModelClass
|
|
64
|
+
)
|
|
65
|
+
tool_wrapper.__signature__ = sig.replace(parameters=(new_param,))
|
|
66
|
+
mcp.add_tool(tool_wrapper)
|
pythia/repository.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
from pythia.datasource import DataSource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Repository(ABC):
|
|
8
|
+
"""Data access layer. Wraps a DataSource and exposes a get() method. Subclass names must end with 'Repository'."""
|
|
9
|
+
|
|
10
|
+
def __init_subclass__(cls, **kwargs):
|
|
11
|
+
super().__init_subclass__(**kwargs)
|
|
12
|
+
if not cls.__name__.endswith("Repository"):
|
|
13
|
+
raise TypeError(
|
|
14
|
+
f"Repository subclasses must end with 'Repository' "
|
|
15
|
+
f"(got: '{cls.__name__}'). Rename to '{cls.__name__}Repository'."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def __init__(self, data_source: DataSource = None):
|
|
19
|
+
if data_source is not None:
|
|
20
|
+
resolved = data_source
|
|
21
|
+
else:
|
|
22
|
+
default = getattr(type(self), "data_source", None)
|
|
23
|
+
resolved = copy.copy(default)
|
|
24
|
+
|
|
25
|
+
if not resolved:
|
|
26
|
+
raise ValueError(f"{self.__class__.__name__}: data_source is required")
|
|
27
|
+
if not isinstance(resolved, DataSource):
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"{self.__class__.__name__}: data_source must be a DataSource instance"
|
|
30
|
+
)
|
|
31
|
+
self.data_source = resolved
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def get(self, **kwargs):
|
|
35
|
+
pass
|
pythia/rest.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, List
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
6
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _validate_api_key(raw_key: str) -> bool:
|
|
10
|
+
env_keys = os.getenv("AUTH_API_KEY", "")
|
|
11
|
+
return bool(raw_key) and raw_key in [k.strip() for k in env_keys.split(",") if k.strip()]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _auth_dependency(request: Request):
|
|
15
|
+
if not os.getenv("AUTH_API_KEY"):
|
|
16
|
+
return
|
|
17
|
+
auth_header = request.headers.get("Authorization")
|
|
18
|
+
api_key = auth_header.split(" ")[1] if auth_header and auth_header.startswith("Bearer ") else None
|
|
19
|
+
if not api_key or not _validate_api_key(api_key):
|
|
20
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RestApp:
|
|
24
|
+
"""FastAPI application with CORS, auth dependency, and default routes (/health, /mcp/tools)."""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.app = FastAPI()
|
|
28
|
+
cors_origins = [o.strip() for o in os.getenv("CORS_ORIGINS", "*").split(",")]
|
|
29
|
+
self.app.add_middleware(
|
|
30
|
+
CORSMiddleware,
|
|
31
|
+
allow_origins=cors_origins,
|
|
32
|
+
allow_methods=["*"],
|
|
33
|
+
allow_headers=["*"],
|
|
34
|
+
)
|
|
35
|
+
self.url_handlers: List[Any] = []
|
|
36
|
+
self._setup_default_routes()
|
|
37
|
+
|
|
38
|
+
def _setup_default_routes(self):
|
|
39
|
+
@self.app.get("/mcp/tools")
|
|
40
|
+
def list_tools():
|
|
41
|
+
return {
|
|
42
|
+
"tools": [
|
|
43
|
+
{
|
|
44
|
+
"name": h.mcp_definition["name"],
|
|
45
|
+
"description": h.mcp_definition["description"],
|
|
46
|
+
"parameters": h.mcp_definition["parameters"],
|
|
47
|
+
"returns": h.mcp_definition.get("returns", {}),
|
|
48
|
+
}
|
|
49
|
+
for h in self.url_handlers
|
|
50
|
+
],
|
|
51
|
+
"server": "pythia",
|
|
52
|
+
"version": "0.1.0",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@self.app.get("/health")
|
|
56
|
+
def health_check():
|
|
57
|
+
return {
|
|
58
|
+
"status": "healthy",
|
|
59
|
+
"timestamp": dt.datetime.utcnow().isoformat(),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def register_handler(self, handler: Any):
|
|
63
|
+
self.url_handlers.append(handler)
|
pythia/server.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from pythia.rest import RestApp
|
|
4
|
+
from pythia.mcp import McpApp
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Server:
|
|
8
|
+
"""Singleton that composes RestApp and McpApp. Entry point for starting the server and accessing registered endpoints."""
|
|
9
|
+
|
|
10
|
+
_instance: Optional["Server"] = None
|
|
11
|
+
|
|
12
|
+
def __new__(cls):
|
|
13
|
+
if cls._instance is None:
|
|
14
|
+
cls._instance = super().__new__(cls)
|
|
15
|
+
cls._instance._initialized = False
|
|
16
|
+
return cls._instance
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
if self._initialized:
|
|
20
|
+
return
|
|
21
|
+
self._rest = RestApp()
|
|
22
|
+
self._mcp = McpApp()
|
|
23
|
+
self._initialized = True
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def app(self):
|
|
27
|
+
return self._rest.app
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def url_handlers(self):
|
|
31
|
+
return self._rest.url_handlers
|
|
32
|
+
|
|
33
|
+
def register_url_handler(self, handler: Any):
|
|
34
|
+
self._rest.register_handler(handler)
|
|
35
|
+
|
|
36
|
+
def start(self, host: str = "0.0.0.0", port: int = 5000, reload: bool = False):
|
|
37
|
+
import uvicorn
|
|
38
|
+
uvicorn.run(self.app, host=host, port=port, reload=reload)
|
|
39
|
+
|
|
40
|
+
def get_mcp(self):
|
|
41
|
+
return self._mcp.build(self.url_handlers)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def get_instance(cls) -> "Server":
|
|
45
|
+
if cls._instance is None:
|
|
46
|
+
cls._instance = cls()
|
|
47
|
+
return cls._instance
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def _reset(cls):
|
|
51
|
+
cls._instance = None
|
pythia/service.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from abc import ABC
|
|
3
|
+
|
|
4
|
+
from pythia.repository import Repository
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Service(ABC):
|
|
8
|
+
"""Business logic layer. Auto-discovers and copies Repository class attributes with DI support. Subclass names must end with 'Service'."""
|
|
9
|
+
|
|
10
|
+
def __init_subclass__(cls, **kwargs):
|
|
11
|
+
super().__init_subclass__(**kwargs)
|
|
12
|
+
if not cls.__name__.endswith("Service"):
|
|
13
|
+
raise TypeError(
|
|
14
|
+
f"Service subclasses must end with 'Service' "
|
|
15
|
+
f"(got: '{cls.__name__}'). Rename to '{cls.__name__}Service'."
|
|
16
|
+
)
|
|
17
|
+
has_repo = any(
|
|
18
|
+
isinstance(v, Repository)
|
|
19
|
+
for klass in cls.__mro__
|
|
20
|
+
if klass not in (Service, object)
|
|
21
|
+
for v in vars(klass).values()
|
|
22
|
+
)
|
|
23
|
+
if not has_repo:
|
|
24
|
+
raise TypeError(
|
|
25
|
+
f"{cls.__name__}: must define at least one Repository instance "
|
|
26
|
+
f"as a class attribute."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def __init__(self, **overrides):
|
|
30
|
+
seen = set()
|
|
31
|
+
for klass in type(self).__mro__:
|
|
32
|
+
for name, value in vars(klass).items():
|
|
33
|
+
if name not in seen and isinstance(value, Repository):
|
|
34
|
+
seen.add(name)
|
|
35
|
+
setattr(self, name, overrides.get(name, copy.copy(value)))
|
pythia/types.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any, Dict, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class McpProperty(TypedDict, total=False):
|
|
5
|
+
type: str
|
|
6
|
+
description: str
|
|
7
|
+
default: Any
|
|
8
|
+
items: Dict[str, Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class McpParameters(TypedDict, total=False):
|
|
12
|
+
properties: Dict[str, McpProperty]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _McpDefinitionBase(TypedDict):
|
|
16
|
+
name: str
|
|
17
|
+
description: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class McpDefinition(_McpDefinitionBase, total=False):
|
|
21
|
+
parameters: McpParameters
|
|
22
|
+
returns: Dict[str, Any]
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: restmcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python framework for building MCP servers with a layered architecture and REST compatibility
|
|
5
|
+
Project-URL: Homepage, https://github.com/JorgeHSantana/Pythia
|
|
6
|
+
Project-URL: Repository, https://github.com/JorgeHSantana/Pythia
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/JorgeHSantana/Pythia/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
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# restmcp
|
|
35
|
+
|
|
36
|
+
> One framework. MCP tools and REST endpoints, auto-registered.
|
|
37
|
+
|
|
38
|
+
Python framework for building **MCP servers** with a layered architecture and REST compatibility.
|
|
39
|
+
Annotated classes become MCP tools and HTTP endpoints: auto-registered, dependency-injected, sync/async agnostic.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Architecture
|
|
44
|
+
|
|
45
|
+
```mermaid
|
|
46
|
+
graph LR
|
|
47
|
+
LLM["🤖 LLM / Client"] -->|"HTTP or MCP"| EP["Endpoint"]
|
|
48
|
+
EP --> SV["Service"]
|
|
49
|
+
SV --> RP["Repository"]
|
|
50
|
+
RP --> DS["DataSource"]
|
|
51
|
+
DS --> EX[("External\nAPI / DB")]
|
|
52
|
+
|
|
53
|
+
style EP fill:#4f46e5,color:#fff,stroke:none
|
|
54
|
+
style SV fill:#7c3aed,color:#fff,stroke:none
|
|
55
|
+
style RP fill:#9333ea,color:#fff,stroke:none
|
|
56
|
+
style DS fill:#a855f7,color:#fff,stroke:none
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install restmcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pythia new my-server
|
|
75
|
+
cd my-server
|
|
76
|
+
pip install -e .
|
|
77
|
+
python main.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Generated structure:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
my-server/
|
|
84
|
+
├── datasource/ # external connections (APIs, databases)
|
|
85
|
+
├── models/ # domain entities (Pydantic)
|
|
86
|
+
├── repositories/ # data access layer
|
|
87
|
+
├── services/ # business logic
|
|
88
|
+
├── tools/ # internal utilities
|
|
89
|
+
├── urls/ # endpoint definitions (auto-discovery)
|
|
90
|
+
├── main.py
|
|
91
|
+
└── pyproject.toml
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## How it works
|
|
97
|
+
|
|
98
|
+
```mermaid
|
|
99
|
+
sequenceDiagram
|
|
100
|
+
participant C as Client / LLM
|
|
101
|
+
participant E as Endpoint
|
|
102
|
+
participant S as Service
|
|
103
|
+
participant R as Repository
|
|
104
|
+
participant D as DataSource
|
|
105
|
+
|
|
106
|
+
C->>E: POST /api/get-product {"product_id": "1"}
|
|
107
|
+
E->>S: service.execute(product_id="1")
|
|
108
|
+
S->>R: repo.get(product_id="1")
|
|
109
|
+
R->>D: data_source.fetch("1")
|
|
110
|
+
D-->>R: raw dict
|
|
111
|
+
R-->>S: ProductEntity
|
|
112
|
+
S-->>E: result dict
|
|
113
|
+
E-->>C: {"tool": "get_product", "result": {...}, "success": true}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Base classes
|
|
119
|
+
|
|
120
|
+
### `DataSource`
|
|
121
|
+
|
|
122
|
+
Abstracts the connection to an external data source (REST API, database, file).
|
|
123
|
+
**Rule:** class name must end with `DataSource`.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import httpx
|
|
127
|
+
from pythia import DataSource
|
|
128
|
+
|
|
129
|
+
class ProductApiDataSource(DataSource):
|
|
130
|
+
base_url = "https://api.example.com"
|
|
131
|
+
|
|
132
|
+
async def fetch(self, product_id: str) -> dict:
|
|
133
|
+
async with httpx.AsyncClient() as client:
|
|
134
|
+
r = await client.get(f"{self.base_url}/products/{product_id}")
|
|
135
|
+
r.raise_for_status()
|
|
136
|
+
return r.json()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### `Entity`
|
|
142
|
+
|
|
143
|
+
Structured domain data backed by Pydantic. Automatic type validation.
|
|
144
|
+
**Rule:** class name must end with `Entity`.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from pythia import Entity
|
|
148
|
+
|
|
149
|
+
class ProductEntity(Entity):
|
|
150
|
+
id: str
|
|
151
|
+
name: str
|
|
152
|
+
price: float
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### `Repository`
|
|
158
|
+
|
|
159
|
+
Fetches data via a `DataSource` and returns `Entity` objects. One source, one data type.
|
|
160
|
+
**Rules:** name ends with `Repository`; must declare `data_source` as class attribute; must implement `get()`.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from pythia import Repository
|
|
164
|
+
from datasource.product_api import ProductApiDataSource
|
|
165
|
+
from models.product import ProductEntity
|
|
166
|
+
|
|
167
|
+
class ProductRepository(Repository):
|
|
168
|
+
data_source = ProductApiDataSource()
|
|
169
|
+
|
|
170
|
+
async def get(self, product_id: str) -> ProductEntity:
|
|
171
|
+
raw = await self.data_source.fetch(product_id)
|
|
172
|
+
return ProductEntity(**raw)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Dependency injection:**
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
repo = ProductRepository() # uses real DataSource
|
|
179
|
+
repo = ProductRepository(data_source=MockDataSource()) # injects mock for tests
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`Repository.__init__` uses `copy.copy()` of the class attribute: instances are always isolated.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### `Service`
|
|
187
|
+
|
|
188
|
+
Orchestrates business logic. Where joins, transformations, and multi-source rules live.
|
|
189
|
+
**Rules:** name ends with `Service`; must declare at least one `Repository` as class attribute.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from pythia import Service
|
|
193
|
+
from repositories.product import ProductRepository
|
|
194
|
+
|
|
195
|
+
class GetProductService(Service):
|
|
196
|
+
repo = ProductRepository()
|
|
197
|
+
|
|
198
|
+
async def execute(self, product_id: str) -> dict:
|
|
199
|
+
product = await self.repo.get(product_id=product_id)
|
|
200
|
+
return product.model_dump()
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Dependency injection:**
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
svc = GetProductService() # production
|
|
207
|
+
svc = GetProductService(repo=MockRepository()) # test
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Repository class attributes are auto-discovered via MRO and isolated per instance.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### `Endpoint`
|
|
215
|
+
|
|
216
|
+
HTTP + MCP route. **Auto-registers on class definition**: no manual wiring needed.
|
|
217
|
+
**Rules:** name ends with `Endpoint`; must declare `mcp_definition`, `url`, `method`, and `callback`.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from pythia import Endpoint
|
|
221
|
+
from services.product import GetProductService
|
|
222
|
+
|
|
223
|
+
class GetProductEndpoint(Endpoint):
|
|
224
|
+
mcp_definition = {
|
|
225
|
+
"name": "get_product",
|
|
226
|
+
"description": "Returns a product by ID",
|
|
227
|
+
"parameters": {
|
|
228
|
+
"properties": {
|
|
229
|
+
"product_id": {"type": "string", "description": "Product ID"},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
url = "/api/get-product"
|
|
234
|
+
method = "POST"
|
|
235
|
+
|
|
236
|
+
async def callback(self, product_id: str) -> dict:
|
|
237
|
+
return await GetProductService().execute(product_id)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Defining the class is enough. The route is registered on the `Server` singleton the moment Python processes the class body.
|
|
241
|
+
|
|
242
|
+
**Disabling an endpoint:**
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
class GetProductEndpoint(Endpoint):
|
|
246
|
+
disabled = True # skips auto-registration; can still be instantiated manually
|
|
247
|
+
...
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Abstract base classes** (missing any required attribute) are never auto-registered:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
class BaseAuthEndpoint(Endpoint):
|
|
254
|
+
method = "POST"
|
|
255
|
+
def callback(self, **kwargs): ...
|
|
256
|
+
# ↑ not registered: url and mcp_definition are missing
|
|
257
|
+
|
|
258
|
+
class GetUserEndpoint(BaseAuthEndpoint):
|
|
259
|
+
mcp_definition = { ... }
|
|
260
|
+
url = "/api/get-user"
|
|
261
|
+
# ↑ registered automatically: all required attributes present
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Sync and async callbacks** are both supported: pythia detects and handles either:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
# sync: runs in a thread pool, does not block the event loop
|
|
268
|
+
def callback(self, product_id: str) -> dict:
|
|
269
|
+
return requests.get(f"https://api.example.com/products/{product_id}").json()
|
|
270
|
+
|
|
271
|
+
# async: awaited directly; use asyncio.gather for parallel I/O
|
|
272
|
+
async def callback(self, product_id: str) -> dict:
|
|
273
|
+
async with httpx.AsyncClient() as client:
|
|
274
|
+
r = await client.get(f"https://api.example.com/products/{product_id}")
|
|
275
|
+
return r.json()
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Response format:**
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{ "tool": "get_product", "result": { ... }, "success": true }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{ "tool": "get_product", "error": "not found", "error_type": "NotFoundError", "success": false }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
### `Server`
|
|
291
|
+
|
|
292
|
+
Singleton with dual-mode: HTTP via FastAPI/uvicorn or MCP protocol via FastMCP.
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from pythia import Server
|
|
296
|
+
import urls # triggers auto-discovery of all endpoint modules
|
|
297
|
+
|
|
298
|
+
server = Server.get_instance()
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
server.start(host="0.0.0.0", port=5000)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
# MCP mode
|
|
306
|
+
mcp = server.get_mcp()
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Built-in routes:**
|
|
310
|
+
|
|
311
|
+
| Route | Method | Auth required |
|
|
312
|
+
|-------|--------|---------------|
|
|
313
|
+
| `/health` | GET | No |
|
|
314
|
+
| `/mcp/tools` | GET | No |
|
|
315
|
+
| _your endpoints_ | POST | Yes (if `AUTH_API_KEY` is set) |
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Exceptions
|
|
320
|
+
|
|
321
|
+
Raised inside `callback`: caught by `Endpoint` and converted to HTTP responses automatically.
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from pythia import ValidationError, NotFoundError
|
|
325
|
+
|
|
326
|
+
raise ValidationError("product_id is required") # → HTTP 400
|
|
327
|
+
raise NotFoundError("Product not found") # → HTTP 404
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```mermaid
|
|
331
|
+
graph TD
|
|
332
|
+
PythiaException --> ValidationError["ValidationError (400)"]
|
|
333
|
+
PythiaException --> NotFoundError["NotFoundError (404)"]
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Testing with injection
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
from pythia import DataSource
|
|
342
|
+
from repositories.product import ProductRepository
|
|
343
|
+
from services.product import GetProductService
|
|
344
|
+
|
|
345
|
+
class FakeProductApiDataSource(DataSource):
|
|
346
|
+
async def fetch(self, product_id: str) -> dict:
|
|
347
|
+
return {"id": product_id, "name": "Test Widget", "price": 1.99}
|
|
348
|
+
|
|
349
|
+
def test_get_product():
|
|
350
|
+
svc = GetProductService(repo=ProductRepository(data_source=FakeProductApiDataSource()))
|
|
351
|
+
result = svc.execute(product_id="1")
|
|
352
|
+
assert result["name"] == "Test Widget"
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Environment variables
|
|
358
|
+
|
|
359
|
+
| Variable | Default | Description |
|
|
360
|
+
|----------|---------|-------------|
|
|
361
|
+
| `AUTH_API_KEY` | _(disabled)_ | Bearer token. Multiple keys supported comma-separated. |
|
|
362
|
+
| `CORS_ORIGINS` | `*` | Allowed origins. Multiple values supported comma-separated. |
|
|
363
|
+
| `LOG_LEVEL` | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`. |
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Naming conventions
|
|
368
|
+
|
|
369
|
+
All base classes enforce a suffix. Violating it raises `TypeError` at import time: before the server starts.
|
|
370
|
+
|
|
371
|
+
| Base class | Required suffix | Example |
|
|
372
|
+
|------------|----------------|---------|
|
|
373
|
+
| `DataSource` | `*DataSource` | `ProductApiDataSource` |
|
|
374
|
+
| `Entity` | `*Entity` | `ProductEntity` |
|
|
375
|
+
| `Repository` | `*Repository` | `ProductRepository` |
|
|
376
|
+
| `Service` | `*Service` | `GetProductService` |
|
|
377
|
+
| `Endpoint` | `*Endpoint` | `GetProductEndpoint` |
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Dependencies
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
fastapi >= 0.100
|
|
385
|
+
uvicorn >= 0.20
|
|
386
|
+
fastmcp >= 2.0
|
|
387
|
+
pydantic >= 2.0
|
|
388
|
+
click >= 8.0
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Author
|
|
394
|
+
|
|
395
|
+
**Jorge Henrique Moreira Santana**
|
|
396
|
+
Electrical Engineer, Postgraduate in Artificial Intelligence
|
|
397
|
+
[LinkedIn](https://www.linkedin.com/in/jorge-santana-b246874a/) · jorge.henrique.moreira.santana@gmail.com
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## License
|
|
402
|
+
|
|
403
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
pythia/__init__.py,sha256=-qBANeCi5gF2DpvgN4JHcQtLWlphZlxvFKFBCN8dOVM,543
|
|
2
|
+
pythia/datasource.py,sha256=yrq96uiFzF4NBoxgHXuxDOd9MNsqwxsVKGUnRS1CqUA,669
|
|
3
|
+
pythia/endpoint.py,sha256=IOt8xSSPDMKTycjqJLPK_u38N_64AK4YYXDNVVM9ZW4,5023
|
|
4
|
+
pythia/entity.py,sha256=yGnit4sD18-n0h-4Umv3QvTf5bo6yFhPlu2IyRaOrwI,474
|
|
5
|
+
pythia/exceptions.py,sha256=sgdbx6PVbYgxXgm1-4I8GUAxgSfPx9IfWlYL11rACRU,549
|
|
6
|
+
pythia/logging.py,sha256=iwGP3p-NrBcvFEL2iEn-ztUHu7e7h3v52aaUBUMVSg8,1174
|
|
7
|
+
pythia/mcp.py,sha256=qkCy18Vpm4Y5Y1ZtyWmRHyUKb7edX6YgGbTWsLWuVOU,2205
|
|
8
|
+
pythia/repository.py,sha256=JXd4T1yCrQJ1tWcEbB12BNv94kfbi_lWq1YA9kJsA1M,1204
|
|
9
|
+
pythia/rest.py,sha256=36L29-K003ZjZnrPe5ytV0BZDpSoPbrCQzSefUyXzig,2143
|
|
10
|
+
pythia/server.py,sha256=n5V43cj8pQ3cqdClqgZ0IA5xUZ6bGGd4wbjxVuFQhuU,1342
|
|
11
|
+
pythia/service.py,sha256=Nflwls_7YNBFLasFmcBL1e0bGZOdBLZobJ4-CDjGTrs,1284
|
|
12
|
+
pythia/types.py,sha256=S9seymj7EzMtRVEOPe8MqkZDwYwZ66yFY4YykO8JPJ8,437
|
|
13
|
+
pythia/cli/__init__.py,sha256=lnmyKZ0xDWdckiWuinpRG1eNnd8-RwEHtpwHX9j0nrY,189
|
|
14
|
+
pythia/cli/new.py,sha256=DajdF4VpEszs5-JO-gn6QGvsNNJsEL9ZoYLn11zP_fg,1504
|
|
15
|
+
restmcp-0.1.0.dist-info/METADATA,sha256=GhMc3KR9N6HuAxC6Gg486xvDSqNB7-Is7WNWkeo7tkk,10892
|
|
16
|
+
restmcp-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
17
|
+
restmcp-0.1.0.dist-info/entry_points.txt,sha256=2yMZUEAaNAPdOuTgqC-wfHLDKbrlR3u8JkMcoqd6YoE,42
|
|
18
|
+
restmcp-0.1.0.dist-info/licenses/LICENSE,sha256=3KqaiOmu_5URYxkqg7EPIQGTd4ckqsu7_P5oCBQ7yuY,1070
|
|
19
|
+
restmcp-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|