Skip to content

Commit 7d09081

Browse files
author
Pedro Rodrigues
committed
local mcp server oauth without api key
1 parent e778675 commit 7d09081

File tree

7 files changed

+906
-30
lines changed

7 files changed

+906
-30
lines changed

src/api/common.py

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from typing import List
22
import requests
33
import json
4+
import logging
45

56
from starlette.exceptions import HTTPException
67
from fastmcp.server.dependencies import get_http_request
78

89
from src.api.types import MCPConcept
910
from src.config.config import get_settings
1011

12+
# Set up logger for this module
13+
logger = logging.getLogger(__name__)
14+
1115

1216
def filter_mcp_concepts(mcp_concepts: List[MCPConcept]) -> List[MCPConcept]:
1317
"""
@@ -16,67 +20,102 @@ def filter_mcp_concepts(mcp_concepts: List[MCPConcept]) -> List[MCPConcept]:
1620
return [mcp_concept for mcp_concept in mcp_concepts if not mcp_concept.deprecated]
1721

1822

19-
def __query_graphql_organizations():
23+
def query_graphql_organizations():
2024
"""
2125
Query the GraphQL endpoint to get a list of organizations the user has access to.
2226
2327
Returns:
2428
List of organizations with their IDs and names
2529
"""
2630
settings = get_settings()
27-
2831
graphql_endpoint = settings.graphql_public_endpoint
2932

33+
logger.debug(f"GraphQL endpoint: {graphql_endpoint}")
34+
logger.debug(f"Settings auth method: {settings.auth_method}")
35+
logger.debug(f"Settings is_remote: {settings.is_remote}")
36+
3037
# GraphQL query for organizations
3138
query = """
32-
query GetOrganizations {
39+
query {
3340
organizations {
3441
orgID
3542
name
3643
}
3744
}
3845
"""
3946

47+
# Get access token with logging
48+
try:
49+
access_token = __get_access_token()
50+
# Only log first/last 8 chars for security
51+
token_preview = (
52+
f"{access_token[:8]}...{access_token[-8:]}"
53+
if len(access_token) > 16
54+
else "***"
55+
)
56+
logger.debug(f"Access token (preview): {token_preview}")
57+
except Exception as e:
58+
logger.error(f"Failed to get access token: {str(e)}")
59+
raise
60+
4061
# Headers with authentication
4162
headers = {
42-
"Authorization": f"Bearer {__get_access_token()}",
63+
"Authorization": f"Bearer {access_token}",
4364
"Content-Type": "application/json",
65+
"Accept": "application/json",
66+
"User-Agent": "SingleStore-MCP-Server",
4467
}
4568

4669
# Payload for the GraphQL request
47-
payload = {
48-
"operationName": "GetOrganizations",
49-
"query": query,
50-
"variables": {},
51-
}
70+
payload = {"query": query.strip()}
71+
72+
logger.debug(f"Request headers: {dict(headers)}")
73+
logger.debug(f"Request payload: {payload}")
5274

5375
try:
76+
logger.debug(f"Making POST request to: {graphql_endpoint}")
77+
78+
# Use the base GraphQL endpoint without query parameters
5479
response = requests.post(
55-
f"{graphql_endpoint}?q=GetOrganizations",
56-
headers=headers,
57-
json=payload,
80+
graphql_endpoint, headers=headers, json=payload, timeout=30
5881
)
5982

83+
logger.debug(f"Response status code: {response.status_code}")
84+
logger.debug(f"Response headers: {dict(response.headers)}")
85+
logger.debug(f"Raw response text: {response.text}")
86+
6087
if response.status_code != 200:
61-
raise ValueError(
62-
f"GraphQL request failed with status code {response.status_code}: {response.text}"
63-
)
88+
error_msg = f"GraphQL request failed with status code {response.status_code}: {response.text}"
89+
logger.error(error_msg)
90+
raise ValueError(error_msg)
6491

6592
data = response.json()
93+
logger.debug(f"Parsed response data: {data}")
94+
6695
if "errors" in data:
6796
errors = data["errors"]
6897
error_message = "; ".join(
6998
[error.get("message", "Unknown error") for error in errors]
7099
)
100+
logger.error(f"GraphQL errors: {errors}")
71101
raise ValueError(f"GraphQL query error: {error_message}")
72102

73103
if "data" in data and "organizations" in data["data"]:
74-
return data["data"]["organizations"]
104+
organizations = data["data"]["organizations"]
105+
logger.info(f"Found {len(organizations)} organizations")
106+
return organizations
75107
else:
108+
logger.warning("No organizations found in response")
76109
return []
77110

111+
except requests.exceptions.RequestException as e:
112+
error_msg = f"Network error when querying organizations: {str(e)}"
113+
logger.error(error_msg)
114+
raise ValueError(error_msg)
78115
except Exception as e:
79-
raise ValueError(f"Failed to query organizations: {str(e)}")
116+
error_msg = f"Failed to query organizations: {str(e)}"
117+
logger.error(error_msg)
118+
raise ValueError(error_msg)
80119

81120

82121
def build_request(
@@ -110,6 +149,13 @@ def build_request_endpoint(endpoint: str, params: dict = None):
110149
# Add organization ID as a query parameter
111150
if settings.is_remote:
112151
params["organizationID"] = settings.org_id
152+
elif (
153+
hasattr(settings, "org_id")
154+
and settings.org_id
155+
and settings.auth_method == "oauth_token"
156+
):
157+
# For local OAuth token authentication, also add organization ID
158+
params["organizationID"] = settings.org_id
113159

114160
if params and type == "GET": # Only add query params for GET requests
115161
url += "?"
@@ -244,6 +290,14 @@ def __get_org_id() -> str:
244290
if settings.is_remote:
245291
return settings.org_id
246292
else:
293+
# For local settings with OAuth token authentication, check if org_id is already set
294+
if (
295+
hasattr(settings, "org_id")
296+
and settings.org_id
297+
and settings.auth_method == "oauth_token"
298+
):
299+
return settings.org_id
300+
247301
organization = build_request("GET", "organizations/current")
248302
if "orgID" in organization:
249303
return organization["orgID"]
@@ -260,14 +314,41 @@ def __get_access_token() -> str:
260314
"""
261315
settings = get_settings()
262316

317+
logger.debug(f"Getting access token, is_remote: {settings.is_remote}")
318+
263319
access_token: str
264320
if settings.is_remote:
265321
request = get_http_request()
266322
access_token = request.headers.get("Authorization", "").replace("Bearer ", "")
323+
logger.debug(
324+
f"Remote access token retrieved (length: {len(access_token) if access_token else 0})"
325+
)
267326
else:
268327
access_token = settings.api_key
328+
logger.debug(
329+
f"Local access token retrieved (length: {len(access_token) if access_token else 0})"
330+
)
269331

270332
if not access_token:
333+
logger.warning("No access token available!")
271334
raise HTTPException(401, "Unauthorized: No access token provided")
272335

273336
return access_token
337+
338+
339+
def get_current_organization():
340+
"""
341+
Get the current organization details from the management API.
342+
343+
Returns:
344+
dict: Organization details including orgID and name
345+
"""
346+
try:
347+
organization = build_request("GET", "organizations/current")
348+
logger.debug(f"Current organization response: {organization}")
349+
return organization
350+
except Exception as e:
351+
logger.error(f"Failed to get current organization: {str(e)}")
352+
raise ValueError(
353+
f"Could not retrieve current organization from the API: {str(e)}"
354+
)

src/api/tools/registery.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from functools import wraps
22
from typing import Callable, List
3+
import inspect
34
from mcp.server.fastmcp import FastMCP
45

56
from src.api.common import filter_mcp_concepts
@@ -8,13 +9,38 @@
89

910

1011
def create_tool_wrapper(func: Callable, name: str, description: str):
11-
@wraps(func)
12-
async def wrapper(*args, **kwargs):
13-
return func(*args, **kwargs)
14-
15-
wrapper.__name__ = name
16-
wrapper.__doc__ = description
17-
return wrapper
12+
# Check if the function is async and has a Context parameter
13+
is_async = inspect.iscoroutinefunction(func)
14+
sig = inspect.signature(func)
15+
has_context = "ctx" in sig.parameters
16+
17+
if is_async and has_context:
18+
# For async functions with Context, keep them as-is since FastMCP handles Context injection
19+
@wraps(func)
20+
async def async_wrapper(*args, **kwargs):
21+
return await func(*args, **kwargs)
22+
23+
async_wrapper.__name__ = name
24+
async_wrapper.__doc__ = description
25+
return async_wrapper
26+
elif has_context:
27+
# For sync functions with Context, wrap to handle Context properly
28+
@wraps(func)
29+
async def sync_with_context_wrapper(*args, **kwargs):
30+
return func(*args, **kwargs)
31+
32+
sync_with_context_wrapper.__name__ = name
33+
sync_with_context_wrapper.__doc__ = description
34+
return sync_with_context_wrapper
35+
else:
36+
# For regular functions without Context
37+
@wraps(func)
38+
async def wrapper(*args, **kwargs):
39+
return func(*args, **kwargs)
40+
41+
wrapper.__name__ = name
42+
wrapper.__doc__ = description
43+
return wrapper
1844

1945

2046
def register_tools(mcp: FastMCP) -> None:

0 commit comments

Comments
 (0)