Skip to content

Commit aa0af3a

Browse files
RodriguespnPedro Rodrigues
and
Pedro Rodrigues
authored
Feature/authentication system (#43)
* update README * Bump version 0.2.0 * oauth implementation for remote server * minor fixes * MCP factory * remove unnecessary attributes from app config * simplified auth_settings * turn resources and tools into a dataclass * moved commands.py to cli * moved logger.py to utils * local server working with one tool * add deprected attribute to tools * auth works for remote server with one tool * add init command to cli * add version to new dir * add the rest of the tools to new_src * add resources and prompts to MCP server * replace src with nw_src * update dockerfile --------- Co-authored-by: Pedro Rodrigues <prodrigues@Pedros-MacBook-Pro.local>
1 parent 2cc9ce4 commit aa0af3a

40 files changed

+720
-2364
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ htmlcov/
3232
# Environment variables
3333
.env
3434
.env.local
35+
.env.remote
3536

3637
# Logs
3738
*.log

Dockerfile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ ADD . /app
88
WORKDIR /app
99
RUN uv sync --locked
1010

11-
ENV SERVER_MODE=http
12-
1311
# Expose the port the MCP server runs on
1412
EXPOSE 8000
1513

16-
CMD ["uv", "run", "src/__main__.py", "start", "--protocol", "http"]
14+
CMD ["uv", "run", "src/main.py", "start", "--transport", "sse"]

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66

77
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.
88

9-
<a href="https://glama.ai/mcp/servers/@singlestore-labs/mcp-server-singlestore">
10-
<img width="380" height="200" src="https://glama.ai/mcp/servers/@singlestore-labs/mcp-server-singlestore/badge" alt="SingleStore Server MCP server" />
11-
</a>
12-
139
## Requirements
1410

1511
- Python >= v3.11.0

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ requires-python = ">=3.10"
77
dependencies = [
88
"black>=25.1.0",
99
"fastapi>=0.115.12",
10+
"fastmcp>=2.5.2",
1011
"flake8>=7.2.0",
1112
"mcp[cli]>=1.8.1",
1213
"nbformat>=5.10.4",
1314
"pydantic-settings>=2.9.1",
1415
"singlestoredb>=1.12.0",
16+
"starlette>=0.46.2",
1517
]
1618

1719
[project.scripts]

src/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
"""SingleStore MCP Server with OAuth"""

src/__main__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/utils/common.py renamed to src/api/common.py

Lines changed: 67 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import requests
22
import json
3-
from src.config import app_config
43

4+
from starlette.exceptions import HTTPException
5+
from fastmcp.server.dependencies import get_http_request
56

6-
def __set_organzation_id():
7-
"""
8-
Set the organization ID for the current session.
9-
"""
10-
if not app_config.is_organization_selected():
11-
select_organization()
7+
from src.config.config import get_settings
128

139

1410
def __query_graphql_organizations():
@@ -18,7 +14,9 @@ def __query_graphql_organizations():
1814
Returns:
1915
List of organizations with their IDs and names
2016
"""
21-
graphql_endpoint = app_config.settings.singlestore_graphql_public_endpoint
17+
settings = get_settings()
18+
19+
graphql_endpoint = settings.graphql_public_endpoint
2220

2321
# GraphQL query for organizations
2422
query = """
@@ -32,7 +30,7 @@ def __query_graphql_organizations():
3230

3331
# Headers with authentication
3432
headers = {
35-
"Authorization": f"Bearer {app_config.get_auth_token()}",
33+
"Authorization": f"Bearer {__get_access_token()}",
3634
"Content-Type": "application/json",
3735
}
3836

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

7472

75-
def select_organization():
76-
"""
77-
Query available organizations and prompt the user to select one.
78-
79-
This must be called after authentication and before making other API calls.
80-
Sets the organization ID and name in the app_config.
81-
82-
Returns:
83-
Dictionary with the selected organization ID and name
84-
"""
85-
86-
print("select_org: ", app_config.organization_id)
87-
# If organization is already selected, return it
88-
if app_config.is_organization_selected():
89-
return {
90-
"orgID": app_config.organization_id,
91-
"name": app_config.organization_name,
92-
}
93-
94-
# Get available organizations
95-
organizations = __query_graphql_organizations()
96-
97-
if not organizations:
98-
raise ValueError("No organizations found. Please check your account access.")
99-
100-
# If only one organization is available, select it automatically
101-
if len(organizations) == 1:
102-
org = organizations[0]
103-
app_config.set_organization(org["orgID"], org["name"])
104-
105-
return {
106-
"orgID": app_config.organization_id,
107-
"name": app_config.organization_name,
108-
}
109-
110-
# Create a formatted list of organizations for the user to choose from
111-
org_list = "\n".join(
112-
[
113-
f"{i + 1}. {org['name']} (ID: {org['orgID']})"
114-
for i, org in enumerate(organizations)
115-
]
116-
)
117-
118-
# This will be handled by the LLM to ask the user which organization to use
119-
raise ValueError(
120-
f"Multiple organizations found. Please ask the user to select one:\n{org_list}"
121-
)
122-
123-
124-
def __build_request(type: str, endpoint: str, params: dict = None, data: dict = None):
73+
def __build_request(
74+
type: str,
75+
endpoint: str,
76+
params: dict = None,
77+
data: dict = None,
78+
):
12579
"""
12680
Make an API request to the SingleStore Management API.
12781
@@ -136,18 +90,19 @@ def __build_request(type: str, endpoint: str, params: dict = None, data: dict =
13690
"""
13791
# Ensure an organization is selected before making API requests
13892

139-
__set_organzation_id()
93+
# __set_organzation_id()
94+
95+
settings = get_settings()
14096

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

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

148-
print(app_config.organization_id)
149-
if app_config.organization_id:
150-
params["organizationID"] = app_config.organization_id
104+
if settings.is_remote:
105+
params["organizationID"] = settings.org_id
151106

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

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

119+
access_token = __get_access_token()
120+
121+
if access_token is not None:
122+
headers["Authorization"] = f"Bearer {access_token}"
123+
165124
request_endpoint = build_request_endpoint(endpoint, params)
166125

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

247206

248-
def __get_project_id():
249-
"""
250-
Get the organization ID (project ID) from the management API.
251-
252-
Returns:
253-
str: The organization ID
254-
"""
255-
# Get current organization info to extract the project ID
256-
org_info = __build_request("GET", "organizations/current")
257-
project_id = org_info.get("orgID")
258-
259-
if not project_id:
260-
raise ValueError("Could not retrieve organization ID from the API")
261-
262-
return project_id
263-
264-
265-
def __get_user_id():
207+
def __get_user_id() -> str:
266208
"""
267209
Get the current user's ID from the management API.
268210
269211
Returns:
270212
str: The user ID
271213
"""
214+
272215
# Get all users in the organization
273216
users = __build_request("GET", "users")
274217

@@ -281,3 +224,44 @@ def __get_user_id():
281224
return user_id
282225

283226
raise ValueError("Could not retrieve user ID from the API")
227+
228+
229+
def __get_org_id() -> str:
230+
"""
231+
Get the organization ID from the management API.
232+
233+
Returns:
234+
str: The organization ID
235+
"""
236+
settings = get_settings()
237+
238+
if settings.is_remote:
239+
return settings.org_id
240+
else:
241+
organization = __build_request("GET", "organizations/current")
242+
if "orgID" in organization:
243+
return organization["orgID"]
244+
else:
245+
raise ValueError("Could not retrieve organization ID from the API")
246+
247+
248+
def __get_access_token() -> str:
249+
"""
250+
Get the access token for the current session.
251+
252+
Returns:
253+
str: The access token
254+
"""
255+
settings = get_settings()
256+
257+
access_token: str
258+
if settings.is_remote:
259+
request = get_http_request()
260+
access_token = request.headers.get("Authorization", "").replace("Bearer ", "")
261+
else:
262+
access_token = settings.api_key
263+
264+
if not access_token:
265+
raise HTTPException(401, "Unauthorized: No access token provided")
266+
267+
return access_token

src/api/resources/register.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import List
33
from mcp.server.fastmcp import FastMCP
44

5-
from src.utils.types import Resource
5+
from src.api.resources.resources import Resource
66

77

88
def register_resources(mcp: FastMCP, resources: List[Resource]) -> None:

src/api/resources/resources.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
from utils.types import Resource
1+
from src.api.resources.types import Resource
2+
23

34
resources_definitions = []
45

5-
resources = [
6-
Resource(
7-
name=resource["name"],
8-
description=resource["description"],
9-
func=resource["func"],
10-
uri=resource["uri"],
11-
)
12-
for resource in resources_definitions
13-
]
6+
resources = [Resource(**resource) for resource in resources_definitions]

src/api/resources/types.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from dataclasses import dataclass
2+
from typing import Callable
3+
4+
from src.api.types import MCPConcept
5+
6+
7+
@dataclass(kw_only=True)
8+
class Resource(MCPConcept):
9+
deprecated: bool
10+
func: Callable
11+
uri: str

src/api/tools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .tools import tools
2-
from .register import register_tools
2+
from .registery import register_tools
33

44
__all__ = ["tools", "register_tools"]

src/api/tools/register.py

Lines changed: 0 additions & 79 deletions
This file was deleted.

0 commit comments

Comments
 (0)