Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
# Build stage
FROM python:3.13-slim AS builder

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy

WORKDIR /build

# Copy project files
COPY pyproject.toml README.md ./
COPY src/ ./src/
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
RUN chmod +x /bin/uv /bin/uvx

COPY pyproject.toml uv.lock README.md src/ ./

RUN uv sync --no-dev --frozen
RUN mkdir /dist
RUN uv export --frozen --no-editable --format requirements.txt | grep -v -e '^[[:space:]]*\.[[:space:]]*$' > /dist/requirements.txt
RUN uv build --wheel -o /dist

# Build wheel
RUN pip install --upgrade pip build && \
python -m build --wheel --outdir /dist

# Runtime stage
FROM python:3.13-slim

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1

# Create non-root user
RUN useradd -m -u 1001 appuser

WORKDIR /app

# Copy wheel from builder
COPY --from=builder /dist/*.whl /tmp/
COPY --from=builder /dist/*.whl /dist/requirements.txt /tmp/

# Install the wheel and dependencies
RUN pip install --no-cache-dir /tmp/*.whl && \
rm /tmp/*.whl
RUN pip install --no-cache-dir -r /tmp/requirements.txt
RUN pip install --no-cache-dir /tmp/dremioai*.whl
RUN rm /tmp/*.whl /tmp/requirements.txt

USER 1001

Expand Down
8 changes: 3 additions & 5 deletions src/dremioai/servers/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,13 @@ class FastMCPServerWithAuthToken(FastMCP):
class DelegatingTokenVerifier(TokenVerifier):
async def verify_token(self, token: str) -> AccessToken | None:
if token:
log.logger("verify_token").info(f"Token verified: {token}")
return AccessToken(
token=token, # Include the token itself
client_id="unused-client",
scopes=["read"],
)
else:
log.logger("verify_token").info(f"Token not provided: {token}")
log.logger("verify_token").info(f"Token not provided")
return None

def streamable_http_app(self):
Expand Down Expand Up @@ -144,6 +143,7 @@ def init(
opts["port"] = port
if host is not None:
opts["host"] = host

mcp = mcp_cls("Dremio", **opts)
if transport == Transports.streamable_http and support_project_id_endpoints:
mcp.support_project_id_endpoints = support_project_id_endpoints
Expand Down Expand Up @@ -229,9 +229,7 @@ def main(
port: Annotated[Optional[int], Option(help="The port to listen on")] = None,
host: Annotated[
Optional[str],
Option(
help="Where uvicorn listens for requests"
),
Option(help="Where uvicorn listens for requests"),
] = "127.0.0.1",
):
log.configure(enable_json_logging=enable_json_logging, to_file=log_to_file)
Expand Down
43 changes: 36 additions & 7 deletions tests/stremable_http_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@

import asyncio
import functools
import json
import random
import time
from contextlib import asynccontextmanager
from typing import Annotated, Optional, AsyncGenerator, Callable, Any
from typing import Annotated, Optional, AsyncGenerator, Callable, Any, Dict
from urllib.parse import urlparse

from mcp import ClientSession
Expand Down Expand Up @@ -123,6 +124,29 @@ async def list_tools(
pp(tool)


@cli.command("call-tool")
@async_command
async def call_tool(
tool: Annotated[str, Option(help="The tool to call")],
url: Annotated[
Optional[str], Option(help="The URL of the MCP server")
] = "http://127.0.0.1:8000/mcp",
token: Annotated[
Optional[str], Option(help="The authorization token to use")
] = None,
args: Annotated[
Optional[str], Option(help="The arguments to pass to the tool as a JSON")
] = None,
):
async with mcp_client_session(url, token) as session:
result = await session.call_tool(tool, json.loads(args) if args else None)
if result.isError:
pp("[red]Error[/red]")
pp(result.content)
return
pp(result.structuredContent["result"])


@app.command("test", help="Run a quick smoketest for a deployed MCP server")
@async_command
async def run_test(
Expand All @@ -142,8 +166,11 @@ async def run_test(
n = int(time.time())
query = f"SELECT {n} as n"
result = await session.call_tool("RunSqlQuery", {"s": query})
result = result.structuredContent["result"]["result"]
pp(result)
if result.isError:
pp("[red]FAIL[/red]")
pp(result.content)
return
pp(result.structuredContent["result"]["result"])

query2 = f"""
SELECT query
Expand All @@ -152,10 +179,12 @@ async def run_test(
and query like '/* dremioai: submitter=RunS%' and query like '%SELECT {n} as n';
"""
result = await session.call_tool("RunSqlQuery", {"s": query2})
result = result.structuredContent["result"]["result"]
pp(result)

if len(result) != 1:
if result.isError:
pp("[red]FAIL[/red]")
pp(result.content)
return
pp(result.structuredContent["result"]["result"])
if len(result.structuredContent["result"]["result"]) != 1:
pp("[red]FAIL[/red]")
pp("[green]OK[/green]")

Expand Down