Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit aef337c

Browse files
committed
Refactor MCP Paradex server for improved performance, security, and user experience, including updates to dependencies, authentication, and tool handling.
1 parent 7469a8e commit aef337c

9 files changed

Lines changed: 142 additions & 342 deletions

File tree

Dockerfile

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ ADD . /app
1717

1818
# Create virtual environment and install the project with its dependencies
1919
RUN --mount=type=cache,target=/root/.cache/uv \
20-
uv venv && \
20+
uv venv --python 3.12 && \
2121
. .venv/bin/activate && \
2222
uv pip install -e .
2323

@@ -27,8 +27,7 @@ RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
2727

2828
WORKDIR /app
2929

30-
COPY --from=uv /root/.local /root/.local
31-
COPY --from=uv --chown=app:app /app/.venv /app/.venv
30+
COPY --from=uv /app/.venv /app/.venv
3231
COPY --from=uv /app /app
3332

3433
# Place executables in the environment at the front of the path
@@ -37,7 +36,5 @@ ENV PATH="/app/.venv/bin:$PATH"
3736
EXPOSE 8080
3837

3938
# Default: stdio (for local use / Claude Desktop).
40-
# For Lambda / remote HTTP: override with
41-
# CMD ["--transport", "streamable-http", "--port", "8080", "--stateless"]
4239
ENTRYPOINT ["mcp-paradex"]
4340
CMD []

src/mcp_paradex/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
MCP Paradex server implementation.
33
"""
44

5-
__version__ = "0.1.1"
5+
__version__ = "0.1.2"

src/mcp_paradex/prompts/__init__.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,42 @@
22
Trading prompts module initialization.
33
"""
44

5-
# Import all prompts to ensure they're registered
5+
# Always available prompts
66
from mcp_paradex.prompts.trader_prompts import (
7-
create_optimal_order,
87
funding_rate_opportunity,
9-
hedging_strategy,
10-
liquidation_protection,
8+
getting_started,
119
market_analysis,
1210
market_overview,
13-
portfolio_risk_assessment,
14-
position_management,
1511
trading_consultation,
1612
vault_analysis,
1713
)
14+
from mcp_paradex.utils.config import config
15+
16+
# Only register auth-required prompts when authenticated
17+
if config.is_configured():
18+
from mcp_paradex.prompts.trader_prompts import (
19+
create_optimal_order,
20+
hedging_strategy,
21+
liquidation_protection,
22+
portfolio_risk_assessment,
23+
position_management,
24+
)
1825

1926
# Export all prompts for convenient importing
2027
__all__ = [
21-
"create_optimal_order",
2228
"funding_rate_opportunity",
23-
"hedging_strategy",
24-
"liquidation_protection",
29+
"getting_started",
2530
"market_analysis",
2631
"market_overview",
27-
"portfolio_risk_assessment",
28-
"position_management",
2932
"trading_consultation",
3033
"vault_analysis",
3134
]
35+
36+
if config.is_configured():
37+
__all__ += [
38+
"create_optimal_order",
39+
"hedging_strategy",
40+
"liquidation_protection",
41+
"portfolio_risk_assessment",
42+
"position_management",
43+
]

src/mcp_paradex/prompts/trader_prompts.py

Lines changed: 51 additions & 303 deletions
Large diffs are not rendered by default.

src/mcp_paradex/server/server.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
from mcp.server.fastmcp.server import FastMCP
1212
from mcp.server.transport_security import TransportSecuritySettings
13+
from starlette.requests import Request
14+
from starlette.responses import Response
15+
from starlette.types import ASGIApp, Receive, Scope, Send
1316

1417
from mcp_paradex import __version__
1518
from mcp_paradex.utils.config import config
@@ -23,6 +26,31 @@
2326
logger = logging.getLogger("mcp-paradex")
2427

2528

29+
class RejectGetMiddleware:
30+
"""
31+
Rejects GET requests with 405 in stateless HTTP mode.
32+
33+
In stateless mode (Lambda), GET /mcp would open a persistent SSE stream that
34+
Lambda cannot hold open. Returning 405 causes MCP clients to fall back to
35+
POST-only mode, which works correctly with Lambda's request/response model.
36+
"""
37+
38+
def __init__(self, app: ASGIApp, mcp_path: str = "/mcp") -> None:
39+
self.app = app
40+
self.mcp_path = mcp_path
41+
42+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
43+
if scope["type"] == "http" and scope["method"] == "GET" and scope["path"] == self.mcp_path:
44+
response = Response(
45+
content="Method Not Allowed: GET is not supported in stateless mode",
46+
status_code=405,
47+
headers={"Allow": "POST"},
48+
)
49+
await response(scope, receive, send)
50+
return
51+
await self.app(scope, receive, send)
52+
53+
2654
def create_server() -> FastMCP:
2755
"""
2856
Create and configure the FastMCP server instance.
@@ -85,6 +113,8 @@ def run_cli() -> None:
85113

86114
try:
87115
if args.transport == "streamable-http":
116+
import uvicorn
117+
88118
server.settings.port = args.port
89119
server.settings.stateless_http = args.stateless
90120
server.settings.host = "0.0.0.0" # bind all interfaces for container deployments
@@ -94,7 +124,29 @@ def run_cli() -> None:
94124
server.settings.transport_security = TransportSecuritySettings(
95125
enable_dns_rebinding_protection=False
96126
)
97-
server.run(transport=args.transport)
127+
starlette_app = server.streamable_http_app()
128+
if args.stateless:
129+
# Wrap with middleware that rejects GET requests.
130+
# In stateless mode GET /mcp would open a persistent SSE stream
131+
# that Lambda cannot hold — 405 makes clients fall back to POST-only.
132+
starlette_app = RejectGetMiddleware(
133+
starlette_app, mcp_path=server.settings.streamable_http_path
134+
)
135+
136+
import anyio
137+
138+
async def _serve() -> None:
139+
config = uvicorn.Config(
140+
starlette_app,
141+
host=server.settings.host,
142+
port=server.settings.port,
143+
log_level=server.settings.log_level.lower(),
144+
)
145+
await uvicorn.Server(config).serve()
146+
147+
anyio.run(_serve)
148+
else:
149+
server.run(transport=args.transport)
98150
except KeyboardInterrupt:
99151
logger.info("Server stopped by user")
100152
except Exception as e:

src/mcp_paradex/tools/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,11 @@
22
Tools module for MCP Paradex.
33
"""
44

5+
from mcp_paradex.utils.config import config
6+
57
# Import tools modules to register them with the server
6-
from . import account, market, orders, system, vaults
8+
from . import market, system, vaults
9+
10+
# Only register auth-required tools when authenticated
11+
if config.is_configured():
12+
from . import account, orders

tests/test_cli.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
# Import the module explicitly so patch() can resolve the dotted path.
1111
import mcp_paradex.server.server as server_module
12-
1312
from mcp_paradex.utils.config import config
1413

1514

tests/test_tools.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import mcp_paradex.utils.paradex_client as _client_module
2525
from mcp_paradex.server.server import server
2626

27-
2827
# ---------------------------------------------------------------------------
2928
# Helpers
3029
# ---------------------------------------------------------------------------
@@ -258,7 +257,9 @@ async def test_markets_pagination(mock_client):
258257
records = [{**MARKET_RECORD, "symbol": f"MKT{i}-USD-PERP"} for i in range(5)]
259258
mock_client.fetch_markets.return_value = {"results": records}
260259

261-
result = await server.call_tool("paradex_markets", {"market_ids": ["ALL"], "limit": 2, "offset": 0})
260+
result = await server.call_tool(
261+
"paradex_markets", {"market_ids": ["ALL"], "limit": 2, "offset": 0}
262+
)
262263
data = _json(result)
263264

264265
assert data["total"] == 5
@@ -302,9 +303,7 @@ async def test_orderbook_passes_depth_param(mock_client):
302303
"bids": [["94999.0", "0.2"]],
303304
}
304305

305-
result = await server.call_tool(
306-
"paradex_orderbook", {"market_id": "BTC-USD-PERP", "depth": 20}
307-
)
306+
result = await server.call_tool("paradex_orderbook", {"market_id": "BTC-USD-PERP", "depth": 20})
308307
data = _json(result)
309308

310309
assert data["market"] == "BTC-USD-PERP"
@@ -490,9 +489,7 @@ async def test_cancel_order_by_client_id(auth_client):
490489
async def test_order_status_by_order_id(auth_client):
491490
auth_client.fetch_order.return_value = ORDER_RECORD
492491

493-
result = await server.call_tool(
494-
"paradex_order_status", {"order_id": "ord-1", "client_id": ""}
495-
)
492+
result = await server.call_tool("paradex_order_status", {"order_id": "ord-1", "client_id": ""})
496493
data = _json(result)
497494

498495
assert data["results"]["id"] == "ord-1"

uv.lock

Lines changed: 0 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)