Files
nocobase-mcp/nocobase_crm_mcp/server.py
T

208 lines
7.0 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 dependencies with: uv sync")
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()