Skip to content

Feature/authentication system #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 1, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ htmlcov/
# Environment variables
.env
.env.local
.env.remote

# Logs
*.log
Expand Down
4 changes: 1 addition & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ ADD . /app
WORKDIR /app
RUN uv sync --locked

ENV SERVER_MODE=http

# Expose the port the MCP server runs on
EXPOSE 8000

CMD ["uv", "run", "src/__main__.py", "start", "--protocol", "http"]
CMD ["uv", "run", "src/main.py", "start", "--transport", "sse"]
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@

With MCP, you can use Claude Desktop, Cursor, or any compatible MCP client to interact with SingleStore using natural language, making it easier to perform complex operations effortlessly.

<a href="https://glama.ai/mcp/servers/@singlestore-labs/mcp-server-singlestore">
<img width="380" height="200" src="https://glama.ai/mcp/servers/@singlestore-labs/mcp-server-singlestore/badge" alt="SingleStore Server MCP server" />
</a>

## Requirements

- Python >= v3.11.0
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ requires-python = ">=3.10"
dependencies = [
"black>=25.1.0",
"fastapi>=0.115.12",
"fastmcp>=2.5.2",
"flake8>=7.2.0",
"mcp[cli]>=1.8.1",
"nbformat>=5.10.4",
"pydantic-settings>=2.9.1",
"singlestoredb>=1.12.0",
"starlette>=0.46.2",
]

[project.scripts]
Expand Down
1 change: 0 additions & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
"""SingleStore MCP Server with OAuth"""
5 changes: 0 additions & 5 deletions src/__main__.py

This file was deleted.

150 changes: 67 additions & 83 deletions src/utils/common.py → src/api/common.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import requests
import json
from src.config import app_config

from starlette.exceptions import HTTPException
from fastmcp.server.dependencies import get_http_request

def __set_organzation_id():
"""
Set the organization ID for the current session.
"""
if not app_config.is_organization_selected():
select_organization()
from src.config.config import get_settings


def __query_graphql_organizations():
Expand All @@ -18,7 +14,9 @@ def __query_graphql_organizations():
Returns:
List of organizations with their IDs and names
"""
graphql_endpoint = app_config.settings.singlestore_graphql_public_endpoint
settings = get_settings()

graphql_endpoint = settings.graphql_public_endpoint

# GraphQL query for organizations
query = """
Expand All @@ -32,7 +30,7 @@ def __query_graphql_organizations():

# Headers with authentication
headers = {
"Authorization": f"Bearer {app_config.get_auth_token()}",
"Authorization": f"Bearer {__get_access_token()}",
"Content-Type": "application/json",
}

Expand Down Expand Up @@ -72,56 +70,12 @@ def __query_graphql_organizations():
raise ValueError(f"Failed to query organizations: {str(e)}")


def select_organization():
"""
Query available organizations and prompt the user to select one.

This must be called after authentication and before making other API calls.
Sets the organization ID and name in the app_config.

Returns:
Dictionary with the selected organization ID and name
"""

print("select_org: ", app_config.organization_id)
# If organization is already selected, return it
if app_config.is_organization_selected():
return {
"orgID": app_config.organization_id,
"name": app_config.organization_name,
}

# Get available organizations
organizations = __query_graphql_organizations()

if not organizations:
raise ValueError("No organizations found. Please check your account access.")

# If only one organization is available, select it automatically
if len(organizations) == 1:
org = organizations[0]
app_config.set_organization(org["orgID"], org["name"])

return {
"orgID": app_config.organization_id,
"name": app_config.organization_name,
}

# Create a formatted list of organizations for the user to choose from
org_list = "\n".join(
[
f"{i + 1}. {org['name']} (ID: {org['orgID']})"
for i, org in enumerate(organizations)
]
)

# This will be handled by the LLM to ask the user which organization to use
raise ValueError(
f"Multiple organizations found. Please ask the user to select one:\n{org_list}"
)


def __build_request(type: str, endpoint: str, params: dict = None, data: dict = None):
def __build_request(
type: str,
endpoint: str,
params: dict = None,
data: dict = None,
):
"""
Make an API request to the SingleStore Management API.

Expand All @@ -136,18 +90,19 @@ def __build_request(type: str, endpoint: str, params: dict = None, data: dict =
"""
# Ensure an organization is selected before making API requests

__set_organzation_id()
# __set_organzation_id()

settings = get_settings()

def build_request_endpoint(endpoint: str, params: dict = None):
url = f"{app_config.settings.singlestore_api_base_url}/v1/{endpoint}"
url = f"{settings.s2_api_base_url}/v1/{endpoint}"

# Add organization ID as a query parameter
if params is None:
params = {}

print(app_config.organization_id)
if app_config.organization_id:
params["organizationID"] = app_config.organization_id
if settings.is_remote:
params["organizationID"] = settings.org_id

if params and type == "GET": # Only add query params for GET requests
url += "?"
Expand All @@ -158,10 +113,14 @@ def build_request_endpoint(endpoint: str, params: dict = None):

# Headers with authentication
headers = {
"Authorization": f"Bearer {app_config.get_auth_token()}",
"Content-Type": "application/json",
}

access_token = __get_access_token()

if access_token is not None:
headers["Authorization"] = f"Bearer {access_token}"

request_endpoint = build_request_endpoint(endpoint, params)

# Default empty JSON body for POST/PUT requests if none provided
Expand Down Expand Up @@ -245,30 +204,14 @@ def __get_workspace_endpoint(
return workspace["endpoint"]


def __get_project_id():
"""
Get the organization ID (project ID) from the management API.

Returns:
str: The organization ID
"""
# Get current organization info to extract the project ID
org_info = __build_request("GET", "organizations/current")
project_id = org_info.get("orgID")

if not project_id:
raise ValueError("Could not retrieve organization ID from the API")

return project_id


def __get_user_id():
def __get_user_id() -> str:
"""
Get the current user's ID from the management API.

Returns:
str: The user ID
"""

# Get all users in the organization
users = __build_request("GET", "users")

Expand All @@ -281,3 +224,44 @@ def __get_user_id():
return user_id

raise ValueError("Could not retrieve user ID from the API")


def __get_org_id() -> str:
"""
Get the organization ID from the management API.

Returns:
str: The organization ID
"""
settings = get_settings()

if settings.is_remote:
return settings.org_id
else:
organization = __build_request("GET", "organizations/current")
if "orgID" in organization:
return organization["orgID"]
else:
raise ValueError("Could not retrieve organization ID from the API")


def __get_access_token() -> str:
"""
Get the access token for the current session.

Returns:
str: The access token
"""
settings = get_settings()

access_token: str
if settings.is_remote:
request = get_http_request()
access_token = request.headers.get("Authorization", "").replace("Bearer ", "")
else:
access_token = settings.api_key

if not access_token:
raise HTTPException(401, "Unauthorized: No access token provided")

return access_token
2 changes: 1 addition & 1 deletion src/api/resources/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import List
from mcp.server.fastmcp import FastMCP

from src.utils.types import Resource
from src.api.resources.resources import Resource


def register_resources(mcp: FastMCP, resources: List[Resource]) -> None:
Expand Down
13 changes: 3 additions & 10 deletions src/api/resources/resources.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
from utils.types import Resource
from src.api.resources.types import Resource


resources_definitions = []

resources = [
Resource(
name=resource["name"],
description=resource["description"],
func=resource["func"],
uri=resource["uri"],
)
for resource in resources_definitions
]
resources = [Resource(**resource) for resource in resources_definitions]
11 changes: 11 additions & 0 deletions src/api/resources/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from dataclasses import dataclass
from typing import Callable

from src.api.types import MCPConcept


@dataclass(kw_only=True)
class Resource(MCPConcept):
deprecated: bool
func: Callable
uri: str
2 changes: 1 addition & 1 deletion src/api/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .tools import tools
from .register import register_tools
from .registery import register_tools

__all__ = ["tools", "register_tools"]
79 changes: 0 additions & 79 deletions src/api/tools/register.py

This file was deleted.

Loading