Skip to content

Commit a74086e

Browse files
authored
Feature/docker api key login (#68)
* add host as cli var * add API key for docker containers * fix docker container start * release: bump version to 0.3.3 (patch release) * remove smithery badge
1 parent b4dd952 commit a74086e

File tree

10 files changed

+103
-123
lines changed

10 files changed

+103
-123
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ RUN uv sync --locked --no-cache
1212
# Expose the port the MCP server runs on
1313
EXPOSE 8000
1414

15-
CMD ["uv", "run", "src/main.py", "start", "--transport", "stdio"]
15+
CMD ["uv", "run", "src/main.py", "start", "--transport", "stdio", "--host", "0.0.0.0"]

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SingleStore MCP Server
22

3-
[![MIT Licence](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/singlestore-labs/mcp-server-singlestore/blob/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/singlestore-mcp-server)](https://pypi.org/project/singlestore-mcp-server/) [![Downloads](https://static.pepy.tech/badge/singlestore-mcp-server)](https://pepy.tech/project/singlestore-mcp-server) [![Smithery](https://smithery.ai/badge/@singlestore-labs/mcp-server-singlestore)](https://smithery.ai/server/@singlestore-labs/mcp-server-singlestore)
3+
[![MIT Licence](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/singlestore-labs/mcp-server-singlestore/blob/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/singlestore-mcp-server)](https://pypi.org/project/singlestore-mcp-server/) [![Downloads](https://static.pepy.tech/badge/singlestore-mcp-server)](https://pepy.tech/project/singlestore-mcp-server)
44

55
[Model Context Protocol]((https://modelcontextprotocol.io/introduction)) (MCP) is a standardized protocol designed to manage context between large language models (LLMs) and external systems. This repository provides an installer and an MCP Server for Singlestore, enabling seamless integration.
66

@@ -231,6 +231,10 @@ To run the Docker container, use the following command:
231231
```bash
232232
docker run -d \
233233
-p 8000:8000 \
234+
-e MCP_API_KEY="your_api_key_here" \
235+
-it \
234236
--name mcp-server \
235237
mcp-server-singlestore
236238
```
239+
240+
Note: An API key is needed when using Docker because the OAuth flow isn't supported locally for servers running in a Docker container. We're working with the Docker team to enable OAuth for local servers in the future. For better security, we recommend using Docker Desktop to configure the S2 MCP server—see [this blog post](https://www.docker.com/blog/docker-mcp-catalog-secure-way-to-discover-and-run-mcp-servers/) for details on Docker's new MCP Catalog.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "SingleStore MCP server"
55
readme = "README.md"
66
requires-python = ">=3.10"
77
dependencies = [
8-
"mcp[cli]>=1.9.4",
8+
"mcp>=1.10.1",
99
"nbformat>=5.10.4",
1010
"pydantic-settings>=2.9.1",
1111
"segment-analytics-python>=2.3.4",

src/api/common.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from starlette.exceptions import HTTPException
66

77
from src.api.types import MCPConcept, AVAILABLE_FLAGS
8+
import src.config.config as config
89
from src.config.config import get_session_request, get_settings
910
from src.logger import get_logger
1011

@@ -208,7 +209,12 @@ def build_request_endpoint(endpoint: str, params: dict = None):
208209
if params is None:
209210
params = {}
210211

211-
params["organizationID"] = get_org_id()
212+
# Get organization ID (might be None for API key auth)
213+
org_id = get_org_id()
214+
215+
# Only add organizationID if it's not None (not using API key)
216+
if org_id is not None:
217+
params["organizationID"] = org_id
212218

213219
if params and type == "GET": # Only add query params for GET requests
214220
url += "?"
@@ -331,15 +337,23 @@ def __get_user_id() -> str:
331337
raise ValueError("Could not retrieve user ID from the API")
332338

333339

334-
def get_org_id() -> str:
340+
def get_org_id() -> str | None:
335341
"""
336342
Get the organization ID from the management API.
337343
344+
For API key authentication, organization ID is not required.
345+
For JWT token authentication, organization ID is required.
346+
338347
Returns:
339-
str: The organization ID
348+
str or None: The organization ID, or None if using API key authentication
340349
"""
341350
settings = get_settings()
342351

352+
# If using API key authentication, no org_id is needed
353+
if not settings.is_remote and settings.api_key:
354+
logger.debug("Using API key authentication, no organization ID needed")
355+
return None
356+
343357
org_id = settings.org_id
344358

345359
if not org_id:
@@ -370,10 +384,15 @@ def get_access_token() -> str:
370384
f"Remote access token retrieved (length: {len(access_token) if access_token else 0})"
371385
)
372386
else:
373-
access_token = settings.jwt_token
374-
logger.debug(
375-
f"Local JWT token retrieved (length: {len(access_token) if access_token else 0})"
376-
)
387+
# Check for API key first, then fall back to JWT token
388+
if isinstance(settings, config.LocalSettings) and settings.api_key:
389+
access_token = settings.api_key
390+
logger.debug("Using API key for authentication")
391+
else:
392+
access_token = settings.jwt_token
393+
logger.debug(
394+
f"Local JWT token retrieved (length: {len(access_token) if access_token else 0})"
395+
)
377396

378397
if not access_token:
379398
logger.warning("No access token available!")

src/api/tools/registery.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,29 @@ def register_tools(mcp: FastMCP, **filter_flags) -> None:
3838
# Register only public tools explicitly
3939
register_tools(mcp, private=False, deprecated=False)
4040
"""
41+
# Import here to avoid circular imports
42+
from src.config.config import get_settings, LocalSettings
43+
4144
# Default: only register public tools (non-private, non-deprecated)
4245
if not filter_flags:
4346
filter_flags = {"internal": False, "deprecated": False}
4447

4548
filtered_tools: List[Tool] = filter_tools(**filter_flags)
4649

50+
# Check if we're using API key authentication in local mode
51+
settings = get_settings()
52+
using_api_key = (
53+
not settings.is_remote
54+
and isinstance(settings, LocalSettings)
55+
and settings.api_key
56+
)
57+
58+
# List of tools to exclude when using API key authentication
59+
api_key_excluded_tools = ["get_organizations", "set_organization"]
60+
4761
for tool in filtered_tools:
4862
func = tool.func
63+
# Skip organization-related tools when using API key authentication
64+
if using_api_key and func.__name__ in api_key_excluded_tools:
65+
continue
4966
mcp.tool(name=func.__name__, description=func.__doc__)(func)

src/commands/start.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from mcp.server.fastmcp import FastMCP
23
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
34

@@ -12,17 +13,27 @@
1213
logger = get_logger()
1314

1415

15-
def start_command(transport):
16-
# Always use browser authentication for stdio mode
16+
def start_command(transport: str, host: str):
17+
api_key = os.environ.get("MCP_API_KEY")
18+
1719
if transport == config.Transport.STDIO:
18-
oauth_token = get_authentication_token()
19-
if not oauth_token:
20-
logger.error("Authentication failed. Please try again")
21-
return
22-
logger.info("Authentication successful")
23-
24-
# Create settings with OAuth token as JWT token
25-
settings = config.init_settings(transport=transport, jwt_token=oauth_token)
20+
if api_key:
21+
# Silent API key authentication for Docker containers
22+
logger.debug("Using API key authentication")
23+
settings = config.init_settings(transport=transport, host=host)
24+
# API key will be automatically loaded from env vars via Pydantic
25+
else:
26+
# Use browser authentication for stdio mode
27+
oauth_token = get_authentication_token()
28+
if not oauth_token:
29+
logger.error("Authentication failed. Please try again")
30+
return
31+
logger.info("Authentication successful")
32+
33+
# Create settings with OAuth token as JWT token
34+
settings = config.init_settings(
35+
transport=transport, jwt_token=oauth_token, host=host
36+
)
2637
else:
2738
raise NotImplementedError("Only stdio transport is currently supported.")
2839
# settings = config.init_settings(transport=transport, jwt_token=None)

src/config/config.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ class Settings(ABC, BaseSettings):
3131
class LocalSettings(Settings):
3232
jwt_token: str | None = None
3333
org_id: str | None = None
34+
api_key: str | None = None
3435
transport: Transport = Transport.STDIO
3536
is_remote: bool = False
3637

37-
# Remove environment variable configuration to force browser auth
38-
# model_config = SettingsConfigDict(env_prefix="MCP_", env_file=".env.local")
38+
# Environment variable configuration for Docker use cases
39+
model_config = SettingsConfigDict(env_prefix="MCP_")
3940

4041
def set_jwt_token(self, token: str) -> None:
4142
"""Set JWT token for authentication (obtained via browser OAuth)"""
@@ -51,7 +52,6 @@ def validate_org_id_uuid(cls, v):
5152

5253

5354
class RemoteSettings(Settings):
54-
host: str
5555
org_id: str
5656
is_remote: bool = True
5757
issuer_url: str
@@ -124,15 +124,15 @@ def get_user_id() -> str | None:
124124

125125

126126
def init_settings(
127-
transport: Transport, jwt_token: str | None = None
127+
transport: Transport, jwt_token: str | None = None, host: str | None = None
128128
) -> RemoteSettings | LocalSettings:
129129
match transport:
130130
case Transport.HTTP:
131131
settings = RemoteSettings(transport=Transport.HTTP)
132132
case Transport.SSE:
133133
settings = RemoteSettings(transport=Transport.SSE)
134134
case Transport.STDIO:
135-
settings = LocalSettings(jwt_token=jwt_token)
135+
settings = LocalSettings(jwt_token=jwt_token, host=host)
136136
case _:
137137
raise ValueError(f"Unsupported transport mode: {transport}")
138138

src/main.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,19 @@ def cli():
2727
default=DEFAULT_TRANSPORT,
2828
help="Only stdio transport is currently supported for local development. ",
2929
)
30-
def start(transport: str):
30+
@click.option(
31+
"--host",
32+
type=str,
33+
default="localhost",
34+
help="Host to bind the MCP server to (default: localhost)",
35+
)
36+
def start(transport: str, host: str):
3137
"""
3238
Start the MCP server with the specified transport.
3339
3440
The server will automatically handle authentication via browser OAuth.
3541
"""
36-
37-
# transport is already a string, no need to convert
38-
logger.info(f"Starting MCP server with transport={transport}")
39-
start_command(transport)
42+
start_command(transport, host)
4043

4144

4245
@cli.command()

src/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.2"
1+
__version__ = "0.3.3"

0 commit comments

Comments
 (0)