208 lines
7.1 KiB
Python
208 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from mcp.server.fastmcp import FastMCP
|
|
from mcp.server.fastmcp.server import TransportSecuritySettings
|
|
|
|
from .config import load_config
|
|
from .crm import CrmService
|
|
from .nb_client import NbClient
|
|
from .nocobase_http import NocoBaseHttpClient
|
|
from .projects import ProjectService
|
|
from .server_runtime import (
|
|
DEFAULT_HTTP_HOST,
|
|
DEFAULT_HTTP_PATH,
|
|
DEFAULT_HTTP_PORT,
|
|
DEFAULT_TOKEN_ENV,
|
|
ENABLE_UPLOADS_ENV,
|
|
BearerTokenAuthMiddleware,
|
|
_read_nocobase_environment,
|
|
_read_token_from_env,
|
|
create_http_app,
|
|
)
|
|
from .server_tools_core import register_core_tools
|
|
from .server_tools_projects import register_project_tools
|
|
from .server_tools_records import register_record_tools
|
|
from .server_tools_write import register_write_tools
|
|
from .tool_errors import _safe_raw_tool_result, _safe_tool_result
|
|
from .uploads import UploadEndpoint
|
|
|
|
ENABLE_CONFIG_TOOLS_ENV = "NOCOBASE_MCP_ENABLE_CONFIG_TOOLS"
|
|
|
|
__all__ = [
|
|
"BearerTokenAuthMiddleware",
|
|
"DEFAULT_HTTP_HOST",
|
|
"DEFAULT_HTTP_PATH",
|
|
"DEFAULT_HTTP_PORT",
|
|
"FastMCP",
|
|
"_make_write_services",
|
|
"_read_nocobase_environment",
|
|
"create_http_app",
|
|
"create_server",
|
|
"run_http_server",
|
|
]
|
|
|
|
|
|
def _make_service(config_path: str | Path | None = None) -> CrmService:
|
|
config = load_config(config_path)
|
|
nb_bin = os.environ.get("NOCOBASE_MCP_NB_BIN", "").strip() or "nb"
|
|
client = NbClient(env_name=config.env_name, data_source=config.data_source, nb_bin=nb_bin)
|
|
return CrmService(config, client)
|
|
|
|
|
|
def _make_write_services(
|
|
config_path: str | Path | None = None,
|
|
*,
|
|
api_base_url: str | None = None,
|
|
api_token: str | None = None,
|
|
) -> tuple[CrmService, ProjectService, NocoBaseHttpClient]:
|
|
config = load_config(config_path)
|
|
nb_bin = os.environ.get("NOCOBASE_MCP_NB_BIN", "").strip() or "nb"
|
|
client = NbClient(
|
|
env_name=config.env_name,
|
|
data_source=config.data_source,
|
|
nb_bin=nb_bin,
|
|
api_base_url=api_base_url,
|
|
api_token=api_token,
|
|
)
|
|
if api_base_url and api_token:
|
|
http_client = NocoBaseHttpClient(api_base_url, api_token)
|
|
else:
|
|
http_client = NocoBaseHttpClient(credential_provider=client.environment_credentials)
|
|
return CrmService(config, client), ProjectService(config.project, client, http_client), http_client
|
|
|
|
|
|
def create_server(
|
|
*,
|
|
crm_service: CrmService | None = None,
|
|
project_service: ProjectService | None = None,
|
|
config_path: str | Path | None = None,
|
|
readonly: bool = True,
|
|
host: str = DEFAULT_HTTP_HOST,
|
|
port: int = DEFAULT_HTTP_PORT,
|
|
streamable_http_path: str = DEFAULT_HTTP_PATH,
|
|
allowed_hosts: list[str] | None = None,
|
|
allowed_origins: list[str] | None = None,
|
|
) -> Any:
|
|
if FastMCP is None:
|
|
raise RuntimeError("Missing MCP SDK. Install runtime dependencies with: uv sync --locked --no-dev")
|
|
service = crm_service or _make_service(config_path)
|
|
if not readonly and project_service is None:
|
|
raise ValueError("project_service is required when readonly is false")
|
|
|
|
transport_security = None
|
|
if TransportSecuritySettings is not None and (allowed_hosts is not None or allowed_origins is not None):
|
|
transport_security = TransportSecuritySettings(
|
|
allowed_hosts=allowed_hosts or [],
|
|
allowed_origins=allowed_origins or [],
|
|
)
|
|
server = FastMCP(
|
|
"nocobase-crm",
|
|
host=host,
|
|
port=port,
|
|
streamable_http_path=streamable_http_path,
|
|
transport_security=transport_security,
|
|
)
|
|
|
|
register_core_tools(
|
|
server,
|
|
service,
|
|
_safe_tool_result,
|
|
enable_config_tools=os.environ.get(ENABLE_CONFIG_TOOLS_ENV, "").strip() == "1",
|
|
)
|
|
register_record_tools(server, service, _safe_tool_result, _safe_raw_tool_result)
|
|
register_project_tools(server, service, _safe_tool_result, _safe_raw_tool_result)
|
|
if not readonly:
|
|
register_write_tools(server, project_service, _safe_tool_result)
|
|
return server
|
|
|
|
|
|
def run_http_server(
|
|
*,
|
|
config_path: Path | None,
|
|
host: str,
|
|
port: int,
|
|
token_env: str,
|
|
streamable_http_path: str,
|
|
allowed_hosts: list[str] | None,
|
|
allowed_origins: list[str] | None,
|
|
) -> None:
|
|
import uvicorn
|
|
|
|
token = _read_token_from_env(token_env)
|
|
api_base_url, api_token, max_bytes = _read_nocobase_environment()
|
|
crm_service, project_service, http_client = _make_write_services(
|
|
config_path,
|
|
api_base_url=api_base_url,
|
|
api_token=api_token,
|
|
)
|
|
server = create_server(
|
|
crm_service=crm_service,
|
|
project_service=project_service,
|
|
readonly=False,
|
|
host=host,
|
|
port=port,
|
|
streamable_http_path=streamable_http_path,
|
|
allowed_hosts=allowed_hosts,
|
|
allowed_origins=allowed_origins,
|
|
)
|
|
upload_endpoint = None
|
|
if os.environ.get(ENABLE_UPLOADS_ENV, "").strip() == "1":
|
|
upload_endpoint = UploadEndpoint(http_client, max_bytes=max_bytes, temp_dir=Path(tempfile.gettempdir()))
|
|
app = create_http_app(server, token, upload_endpoint=upload_endpoint)
|
|
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Run the local NocoBase CRM MCP server.")
|
|
parser.add_argument("--config", default=None, type=Path, help="CRM mapping JSON config path.")
|
|
parser.add_argument(
|
|
"--transport",
|
|
choices=["stdio", "streamable-http"],
|
|
default="stdio",
|
|
help="MCP transport. HTTP mode requires a bearer token.",
|
|
)
|
|
parser.add_argument("--host", default=DEFAULT_HTTP_HOST, help="HTTP bind host.")
|
|
parser.add_argument("--port", default=DEFAULT_HTTP_PORT, type=int, help="HTTP port.")
|
|
parser.add_argument("--path", default=DEFAULT_HTTP_PATH, help="Streamable HTTP MCP path.")
|
|
parser.add_argument(
|
|
"--token-env", default=DEFAULT_TOKEN_ENV, help="Environment variable containing the HTTP bearer token."
|
|
)
|
|
parser.add_argument("--allowed-host", action="append", dest="allowed_hosts")
|
|
parser.add_argument("--allowed-origin", action="append", dest="allowed_origins")
|
|
parser.add_argument(
|
|
"--enable-writes",
|
|
action="store_true",
|
|
help="Expose fixed-schema project create/update tools in local stdio mode.",
|
|
)
|
|
args = parser.parse_args()
|
|
if args.transport == "stdio":
|
|
if args.enable_writes:
|
|
crm_service, project_service, _http_client = _make_write_services(args.config)
|
|
create_server(
|
|
crm_service=crm_service,
|
|
project_service=project_service,
|
|
readonly=False,
|
|
).run(transport="stdio")
|
|
else:
|
|
create_server(config_path=args.config).run(transport="stdio")
|
|
return
|
|
run_http_server(
|
|
config_path=args.config,
|
|
host=args.host,
|
|
port=args.port,
|
|
token_env=args.token_env,
|
|
streamable_http_path=args.path,
|
|
allowed_hosts=args.allowed_hosts,
|
|
allowed_origins=args.allowed_origins,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|