Skip to content

Commit c820ca9

Browse files
committed
feat (server-revamp): added gshopping mcp
1 parent 2b124f7 commit c820ca9

File tree

15 files changed

+354
-13
lines changed

15 files changed

+354
-13
lines changed

src/client/app/api/chat/message/route.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export async function POST(request) {
1818
enable_internet,
1919
enable_weather,
2020
enable_news,
21-
enable_maps
21+
enable_maps,
22+
enable_shopping
2223
} = await request.json()
2324
const authHeader = await getBackendAuthHeader()
2425

@@ -47,7 +48,8 @@ export async function POST(request) {
4748
enable_internet,
4849
enable_weather,
4950
enable_news,
50-
enable_maps
51+
enable_maps,
52+
enable_shopping
5153
}),
5254
// IMPORTANT: duplex must be set to 'half' to stream response body in Next.js Edge/Node runtime
5355
duplex: "half"

src/client/app/chat/[[...chatId]]/page.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
IconPresentation,
2424
IconTable,
2525
IconMessageOff,
26-
IconMap
26+
IconMap,
27+
IconShoppingCart
2728
} from "@tabler/icons-react"
2829
import toast from "react-hot-toast"
2930
import GmailSearchResults from "@components/agents/GmailSearchResults"
@@ -66,6 +67,7 @@ const Chat = ({ params }) => {
6667
const [isWeatherEnabled, setWeatherEnabled] = useState(false)
6768
const [isNewsEnabled, setNewsEnabled] = useState(false)
6869
const [isMapsEnabled, setMapsEnabled] = useState(false)
70+
const [isShoppingEnabled, setShoppingEnabled] = useState(false)
6971

7072
// --- Refs ---
7173
const textareaRef = useRef(null)
@@ -272,7 +274,8 @@ const Chat = ({ params }) => {
272274
enable_internet: isInternetEnabled,
273275
enable_weather: isWeatherEnabled,
274276
enable_news: isNewsEnabled,
275-
enable_maps: isMapsEnabled
277+
enable_maps: isMapsEnabled,
278+
enable_shopping: isShoppingEnabled
276279
}),
277280
signal: abortControllerRef.current.signal
278281
})
@@ -635,7 +638,7 @@ const Chat = ({ params }) => {
635638
>
636639
{/* Tool Toggles & Info */}
637640
<div className="flex flex-col items-center">
638-
<div className="flex items-center gap-4 mb-3 text-xs text-gray-400">
641+
<div className="flex items-center flex-wrap justify-center gap-4 mb-3 text-xs text-gray-400">
639642
<label
640643
htmlFor="internet-toggle"
641644
className="flex items-center gap-1.5 cursor-pointer hover:text-white transition-colors"
@@ -720,6 +723,28 @@ const Chat = ({ params }) => {
720723
}
721724
/>
722725
</label>
726+
<label
727+
htmlFor="shopping-toggle"
728+
className="flex items-center gap-1.5 cursor-pointer hover:text-white transition-colors"
729+
>
730+
<IconShoppingCart
731+
size={16}
732+
className={
733+
isShoppingEnabled
734+
? "text-lightblue"
735+
: ""
736+
}
737+
/>
738+
<span>Shopping</span>
739+
<Switch
740+
checked={
741+
isShoppingEnabled
742+
}
743+
onCheckedChange={
744+
setShoppingEnabled
745+
}
746+
/>
747+
</label>
723748
</div>
724749
{connectedIntegrations.length >
725750
0 && (

src/client/app/settings/page.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import {
2727
IconLockOpen,
2828
IconPresentation,
2929
IconTable,
30-
IconMapPin
30+
IconMapPin,
31+
IconShoppingCart
3132
} from "@tabler/icons-react"
3233
import { useState, useEffect, useCallback } from "react"
3334
import Sidebar from "@components/Sidebar"
@@ -43,6 +44,7 @@ const integrationIcons = {
4344
gslides: IconPresentation,
4445
gsheets: IconTable,
4546
gmaps: IconMapPin,
47+
gshopping: IconShoppingCart,
4648
slack: IconBrandSlack,
4749
notion: IconBrandNotion,
4850
accuweather: IconCloud,
@@ -269,8 +271,9 @@ const GoogleAuthSettings = ({ mode, onModeChange, onSaveSuccess }) => {
269271
</li>
270272
<li>
271273
Enable the APIs you want to use (e.g., Gmail API, Google
272-
Drive API, Google Calendar API, Google Maps Platform APIs)
273-
in the "APIs & Services" dashboard.
274+
Drive API, Google Calendar API, Google Maps Platform APIs,
275+
Google Shopping Content API) in the "APIs & Services"
276+
dashboard.
274277
</li>
275278
<li>
276279
Go to "Credentials", click "Create Credentials", and select
@@ -309,6 +312,7 @@ const GoogleAuthSettings = ({ mode, onModeChange, onSaveSuccess }) => {
309312
<li>https://www.googleapis.com/auth/presentations</li>
310313
<li>https://www.googleapis.com/auth/spreadsheets</li>
311314
<li>https://www.googleapis.com/auth/cloud-platform</li>
315+
<li>https://www.googleapis.com/auth/content</li>
312316
</ul>
313317
</li>
314318
</ol>
@@ -479,6 +483,7 @@ const Settings = () => {
479483
"https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive",
480484
gsheets: "https://www.googleapis.com/auth/spreadsheets",
481485
gmaps: "https://www.googleapis.com/auth/cloud-platform",
486+
gshopping: "https://www.googleapis.com/auth/content",
482487
github: "repo user"
483488
}
484489

src/server/.env.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ CHAT_TOOLS_MCP_SERVER_URL=http://localhost:9013/sse
4848
GDOCS_MCP_SERVER_URL=http://localhost:9004/sse
4949
GSLIDES_MCP_SERVER_URL=http://localhost:9014/sse
5050
GSHEETS_MCP_SERVER_URL=http://localhost:9015/sse
51-
GMAPS_MCP_SERVER_URL=http://localhost:9016/sse
51+
GMAPS_MCP_SERVER_URL=http://localhost:9016/sse
52+
GSHOPPING_MCP_SERVER_URL=http://localhost:9017/sse

src/server/main/chat/utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ async def generate_chat_llm_stream(
4040
enable_internet: bool = False,
4141
enable_weather: bool = False,
4242
enable_news: bool = False,
43-
enable_maps: bool = False
43+
enable_maps: bool = False,
44+
enable_shopping: bool = False
4445
) -> AsyncGenerator[Dict[str, Any], None]:
4546
assistant_message_id = assistant_message_id_override or str(uuid.uuid4())
4647

@@ -59,7 +60,8 @@ async def generate_chat_llm_stream(
5960
"internet_search": enable_internet,
6061
"accuweather": enable_weather,
6162
"news": enable_news,
62-
"gmaps": enable_maps
63+
"gmaps": enable_maps,
64+
"gshopping": enable_shopping
6365
}
6466

6567
for service_name, is_enabled in tool_flags.items():
@@ -105,7 +107,8 @@ def worker():
105107
f"- Internet search is currently {'ENABLED' if enable_internet else 'DISABLED'}. You can use it to find real-time information if enabled.\n"
106108
f"- Weather information is currently {'ENABLED' if enable_weather else 'DISABLED'}.\n"
107109
f"- News headlines and articles are currently {'ENABLED' if enable_news else 'DISABLED'}.\n"
108-
f"- Google Maps for places and directions is currently {'ENABLED' if enable_maps else 'DISABLED'}.\n\n"
110+
f"- Google Maps for places and directions is currently {'ENABLED' if enable_maps else 'DISABLED'}.\n"
111+
f"- Google Shopping for product searches is currently {'ENABLED' if enable_shopping else 'DISABLED'}.\n\n"
109112
"Be conversational and helpful."
110113
)
111114
qwen_assistant = get_qwen_assistant(system_message=system_prompt, function_list=tools)

src/server/main/config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@
150150
"url": os.getenv("GMAPS_MCP_SERVER_URL", "http://localhost:9016/sse")
151151
}
152152
},
153+
"gshopping": {
154+
"display_name": "Google Shopping",
155+
"description": "Search for products online.",
156+
"auth_type": "oauth",
157+
"icon": "IconShoppingCart",
158+
"mcp_server_config": {
159+
"name": "gshopping_server",
160+
"url": os.getenv("GSHOPPING_MCP_SERVER_URL", "http://localhost:9017/sse")
161+
}
162+
},
153163
"slack": { # User-configurable Manual
154164
"display_name": "Slack",
155165
"description": "Connect to your Slack workspace to send messages and more.",

src/server/main/misc/routes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ async def _validate_gcp_credentials(creds_json_str: str, user_email: str) -> boo
8989
# We need drive scope for the about.get call and cloud-platform for maps
9090
scopes = [
9191
'https://www.googleapis.com/auth/drive.readonly',
92-
'https://www.googleapis.com/auth/cloud-platform'
92+
'https://www.googleapis.com/auth/cloud-platform',
93+
'https://www.googleapis.com/auth/content'
9394
]
9495

9596
creds = service_account.Credentials.from_service_account_info(
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# HTTP Server configuration for FastMCP
2+
MCP_SERVER_HOST=127.0.0.1
3+
MCP_SERVER_PORT=9017
4+
5+
# This MCP uses the main server's Google credentials, specifically the Custom Search API keys.
6+
# No separate API key is needed if using the Service Account or OAuth flow configured on the main server.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file can remain empty. It marks the 'gshopping' directory as a Python package.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import os
2+
import json
3+
import base64
4+
from typing import Dict, Optional
5+
6+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
7+
from cryptography.hazmat.primitives import padding
8+
from cryptography.hazmat.backends import default_backend
9+
import motor.motor_asyncio
10+
from google.oauth2.credentials import Credentials
11+
from google.oauth2 import service_account
12+
13+
from fastmcp import Context
14+
from fastmcp.exceptions import ToolError
15+
from dotenv import load_dotenv
16+
17+
dotenv_path = os.path.join(os.path.dirname(__file__), '..', '..', '.env')
18+
load_dotenv(dotenv_path=dotenv_path)
19+
20+
# --- Config ---
21+
MONGO_URI = os.getenv("MONGO_URI")
22+
MONGO_DB_NAME = os.getenv("MONGO_DB_NAME")
23+
AES_SECRET_KEY_HEX = os.getenv("AES_SECRET_KEY")
24+
AES_IV_HEX = os.getenv("AES_IV")
25+
# Default keys from the main .env, which the shopping search will use
26+
DEFAULT_GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
27+
DEFAULT_GOOGLE_CSE_ID = os.getenv("GOOGLE_CSE_ID")
28+
29+
AES_SECRET_KEY: Optional[bytes] = bytes.fromhex(AES_SECRET_KEY_HEX) if AES_SECRET_KEY_HEX and len(AES_SECRET_KEY_HEX) == 64 else None
30+
AES_IV: Optional[bytes] = bytes.fromhex(AES_IV_HEX) if AES_IV_HEX and len(AES_IV_HEX) == 32 else None
31+
32+
client = motor.motor_asyncio.AsyncIOMotorClient(MONGO_URI)
33+
db = client[MONGO_DB_NAME]
34+
users_collection = db["user_profiles"]
35+
36+
def aes_decrypt(encrypted_data: str) -> str:
37+
if not AES_SECRET_KEY or not AES_IV:
38+
raise ValueError("AES encryption keys are not configured.")
39+
backend = default_backend()
40+
cipher = Cipher(algorithms.AES(AES_SECRET_KEY), modes.CBC(AES_IV), backend=backend)
41+
decryptor = cipher.decryptor()
42+
encrypted_bytes = base64.b64decode(encrypted_data)
43+
decrypted = decryptor.update(encrypted_bytes) + decryptor.finalize()
44+
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
45+
unpadded_data = unpadder.update(decrypted) + unpadder.finalize()
46+
return unpadded_data.decode()
47+
48+
def get_user_id_from_context(ctx: Context) -> str:
49+
http_request = ctx.get_http_request()
50+
if not http_request:
51+
raise ToolError("HTTP request context is not available.")
52+
user_id = http_request.headers.get("X-User-ID")
53+
if not user_id:
54+
raise ToolError("Authentication failed: 'X-User-ID' header is missing.")
55+
return user_id
56+
57+
async def get_google_api_keys(user_id: str) -> Dict[str, str]:
58+
"""
59+
Retrieves Google API keys. For Shopping, we rely on the Custom Search API keys
60+
which are currently stored globally in the .env file, not per user.
61+
This function validates the user exists before returning the global keys.
62+
"""
63+
user_doc = await users_collection.find_one({"user_id": user_id})
64+
if not user_doc:
65+
raise ToolError(f"User profile not found for user_id: {user_id}.")
66+
67+
if not DEFAULT_GOOGLE_API_KEY or not DEFAULT_GOOGLE_CSE_ID:
68+
raise ToolError("Google Custom Search API Key or CSE ID is not configured on the server.")
69+
70+
return {"api_key": DEFAULT_GOOGLE_API_KEY, "cse_id": DEFAULT_GOOGLE_CSE_ID}

0 commit comments

Comments
 (0)