Only serve worker endpoints if secret is set (#691)
Default to `ARCADE_WORKER_SECRET` being unset. This env var must be explicitly set now. Once it is set, the `worker/` endpoints will be served.
This commit is contained in:
parent
a921b76ce9
commit
44660d18ce
7 changed files with 38 additions and 18 deletions
|
|
@ -383,6 +383,7 @@ def start_server_process(entrypoint: str, debug: bool = False) -> tuple[subproce
|
||||||
"ARCADE_SERVER_PORT": str(port),
|
"ARCADE_SERVER_PORT": str(port),
|
||||||
"ARCADE_SERVER_TRANSPORT": "http",
|
"ARCADE_SERVER_TRANSPORT": "http",
|
||||||
"ARCADE_AUTH_DISABLED": "true",
|
"ARCADE_AUTH_DISABLED": "true",
|
||||||
|
"ARCADE_WORKER_SECRET": "temp-validation-secret",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd = [sys.executable, entrypoint]
|
cmd = [sys.executable, entrypoint]
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,8 @@ class ArcadeSettings(BaseSettings):
|
||||||
description="Disable authentication",
|
description="Disable authentication",
|
||||||
)
|
)
|
||||||
server_secret: str | None = Field(
|
server_secret: str | None = Field(
|
||||||
default="dev",
|
default=None,
|
||||||
description="Server secret",
|
description="Server secret for worker endpoints (required to enable worker routes)",
|
||||||
validation_alias="ARCADE_WORKER_SECRET",
|
validation_alias="ARCADE_WORKER_SECRET",
|
||||||
)
|
)
|
||||||
environment: str = Field(
|
environment: str = Field(
|
||||||
|
|
|
||||||
|
|
@ -123,15 +123,14 @@ def create_arcade_mcp(
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> FastAPI:
|
) -> FastAPI:
|
||||||
"""
|
"""
|
||||||
Create a FastAPI app exposing Arcade Worker and MCP HTTP endpoints.
|
Create a FastAPI app exposing MCP HTTP endpoints
|
||||||
|
and Arcade Worker endpoints if a secret is provided.
|
||||||
|
|
||||||
MCP is always enabled in this integrated application.
|
MCP is always enabled in this integrated application.
|
||||||
"""
|
"""
|
||||||
if mcp_settings is None:
|
if mcp_settings is None:
|
||||||
mcp_settings = MCPSettings.from_env()
|
mcp_settings = MCPSettings.from_env()
|
||||||
secret = mcp_settings.arcade.server_secret
|
secret = mcp_settings.arcade.server_secret
|
||||||
if secret is None:
|
|
||||||
secret = "dev" # noqa: S105
|
|
||||||
|
|
||||||
otel_handler = OTELHandler(
|
otel_handler = OTELHandler(
|
||||||
enable=otel_enable,
|
enable=otel_enable,
|
||||||
|
|
@ -180,13 +179,15 @@ def create_arcade_mcp(
|
||||||
app.add_middleware(AddTrailingSlashToPathMiddleware)
|
app.add_middleware(AddTrailingSlashToPathMiddleware)
|
||||||
|
|
||||||
# Worker endpoints
|
# Worker endpoints
|
||||||
worker = FastAPIWorker(
|
if secret is not None:
|
||||||
app=app,
|
worker = FastAPIWorker(
|
||||||
secret=secret,
|
app=app,
|
||||||
disable_auth=mcp_settings.arcade.auth_disabled,
|
secret=secret,
|
||||||
otel_meter=otel_handler.get_meter(),
|
disable_auth=mcp_settings.arcade.auth_disabled,
|
||||||
)
|
otel_meter=otel_handler.get_meter(),
|
||||||
worker.catalog = catalog
|
)
|
||||||
|
worker.catalog = catalog
|
||||||
|
logger.info("Worker routes enabled at /worker/* (ARCADE_WORKER_SECRET is set)")
|
||||||
|
|
||||||
class _MCPASGIProxy:
|
class _MCPASGIProxy:
|
||||||
def __init__(self, parent_app: FastAPI):
|
def __init__(self, parent_app: FastAPI):
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "arcade-mcp-server"
|
name = "arcade-mcp-server"
|
||||||
version = "1.8.0"
|
version = "1.9.0"
|
||||||
description = "Model Context Protocol (MCP) server framework for Arcade.dev"
|
description = "Model Context Protocol (MCP) server framework for Arcade.dev"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{ name = "Arcade.dev" }]
|
authors = [{ name = "Arcade.dev" }]
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ def start_mcp_server(
|
||||||
"ARCADE_SERVER_PORT": str(port),
|
"ARCADE_SERVER_PORT": str(port),
|
||||||
"ARCADE_SERVER_TRANSPORT": "http",
|
"ARCADE_SERVER_TRANSPORT": "http",
|
||||||
"ARCADE_AUTH_DISABLED": "true",
|
"ARCADE_AUTH_DISABLED": "true",
|
||||||
|
"ARCADE_WORKER_SECRET": "test-secret-e2e",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd = ["uv", "run", entrypoint_path, "http"]
|
cmd = ["uv", "run", entrypoint_path, "http"]
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,17 @@ def mock_router():
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def base_worker(mock_router):
|
def base_worker(mock_router):
|
||||||
|
# Save original value
|
||||||
|
original_secret = os.environ.get("ARCADE_WORKER_SECRET")
|
||||||
# Set env var temporarily for testing secret loading
|
# Set env var temporarily for testing secret loading
|
||||||
os.environ["ARCADE_WORKER_SECRET"] = "test_secret_env" # noqa: S105
|
os.environ["ARCADE_WORKER_SECRET"] = "test_secret_env" # noqa: S105
|
||||||
worker = BaseWorker()
|
worker = BaseWorker()
|
||||||
worker.register_routes(mock_router) # Register routes using the mock router
|
worker.register_routes(mock_router) # Register routes using the mock router
|
||||||
# Clean up env var
|
# Restore original value
|
||||||
del os.environ["ARCADE_WORKER_SECRET"]
|
if original_secret is not None:
|
||||||
|
os.environ["ARCADE_WORKER_SECRET"] = original_secret
|
||||||
|
else:
|
||||||
|
del os.environ["ARCADE_WORKER_SECRET"]
|
||||||
return worker
|
return worker
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -68,20 +73,32 @@ def test_base_worker_init_with_secret():
|
||||||
|
|
||||||
|
|
||||||
def test_base_worker_init_with_env_secret():
|
def test_base_worker_init_with_env_secret():
|
||||||
|
original_secret = os.environ.get("ARCADE_WORKER_SECRET")
|
||||||
os.environ["ARCADE_WORKER_SECRET"] = "env_secret_value" # noqa: S105
|
os.environ["ARCADE_WORKER_SECRET"] = "env_secret_value" # noqa: S105
|
||||||
worker = BaseWorker()
|
worker = BaseWorker()
|
||||||
assert worker.secret == "env_secret_value" # noqa: S105
|
assert worker.secret == "env_secret_value" # noqa: S105
|
||||||
assert not worker.disable_auth
|
assert not worker.disable_auth
|
||||||
del os.environ["ARCADE_WORKER_SECRET"]
|
|
||||||
|
# Restore secret to original if it was set
|
||||||
|
if original_secret is not None:
|
||||||
|
os.environ["ARCADE_WORKER_SECRET"] = original_secret
|
||||||
|
else:
|
||||||
|
del os.environ["ARCADE_WORKER_SECRET"]
|
||||||
|
|
||||||
|
|
||||||
def test_base_worker_init_no_secret_raises_error():
|
def test_base_worker_init_no_secret_raises_error():
|
||||||
# Ensure env var is not set
|
# Ensure secret is not set
|
||||||
|
original_secret = os.environ.get("ARCADE_WORKER_SECRET")
|
||||||
if "ARCADE_WORKER_SECRET" in os.environ:
|
if "ARCADE_WORKER_SECRET" in os.environ:
|
||||||
del os.environ["ARCADE_WORKER_SECRET"]
|
del os.environ["ARCADE_WORKER_SECRET"]
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="No secret provided for worker"):
|
with pytest.raises(ValueError, match="No secret provided for worker"):
|
||||||
BaseWorker()
|
BaseWorker()
|
||||||
|
|
||||||
|
# Restore secret if it was set
|
||||||
|
if original_secret is not None:
|
||||||
|
os.environ["ARCADE_WORKER_SECRET"] = original_secret
|
||||||
|
|
||||||
|
|
||||||
def test_base_worker_init_disable_auth():
|
def test_base_worker_init_disable_auth():
|
||||||
worker = BaseWorker(disable_auth=True)
|
worker = BaseWorker(disable_auth=True)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "arcade-mcp"
|
name = "arcade-mcp"
|
||||||
version = "1.5.3"
|
version = "1.5.4"
|
||||||
description = "Arcade.dev - Tool Calling platform for Agents"
|
description = "Arcade.dev - Tool Calling platform for Agents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue