Skip to content
Draft
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
345 changes: 267 additions & 78 deletions src/client/content/chatbot.py

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions src/client/content/config/mcp_servers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import inspect

from client.mcp.frontend import display_commands_tab, display_ide_tab, get_fastapi_base_url, get_server_capabilities

import streamlit as st

def main():
fastapi_base_url = get_fastapi_base_url()
tools, resources, prompts = get_server_capabilities(fastapi_base_url)
if "chat_history" not in st.session_state:
st.session_state.chat_history = []
ide, commands = st.tabs(["🛠️ IDE", "📚 Available Commands"])

with ide:
# Display the IDE tab using the original AI Optimizer logic.
display_ide_tab()
with commands:
# Display the commands tab using the original AI Optimizer logic.
display_commands_tab(tools, resources, prompts)



if __name__ == "__main__" or "page.py" in inspect.stack()[1].filename:
main()
39 changes: 28 additions & 11 deletions src/client/content/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,32 @@
#############################################################################
def get_settings(include_sensitive: bool = False):
"""Get Server-Side Settings"""
settings = api_call.get(
endpoint="v1/settings",
params={
"client": state.client_settings["client"],
"full_config": True,
"incl_sensitive": include_sensitive,
},
)
return settings
try:
settings = api_call.get(
endpoint="v1/settings",
params={
"client": state.client_settings["client"],
"full_config": True,
"incl_sensitive": include_sensitive,
},
)
return settings
except api_call.ApiError as e:
if "not found" in str(e):
# If client settings not found, create them
logger.info("Client settings not found, creating new ones")
api_call.post(endpoint="v1/settings", params={"client": state.client_settings["client"]})
settings = api_call.get(
endpoint="v1/settings",
params={
"client": state.client_settings["client"],
"full_config": True,
"incl_sensitive": include_sensitive,
},
)
return settings
else:
raise


def save_settings(settings):
Expand Down Expand Up @@ -144,8 +161,8 @@ def spring_ai_conf_check(ll_model: dict, embed_model: dict) -> str:
if not ll_model or not embed_model:
return "hybrid"

ll_api = ll_model["api"]
embed_api = embed_model["api"]
ll_api = ll_model.get("api", "")
embed_api = embed_model.get("api", "")

if "OpenAI" in ll_api and "OpenAI" in embed_api:
return "openai"
Expand Down
446 changes: 446 additions & 0 deletions src/client/mcp/client.py

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions src/client/mcp/frontend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import streamlit as st
import os
import requests
import json

def set_page():
st.set_page_config(
page_title="MCP Universal Chatbot",
page_icon="🤖",
layout="wide"
)

def get_fastapi_base_url():
return os.getenv("FASTAPI_BASE_URL", "http://127.0.0.1:8000")

@st.cache_data(show_spinner="Connecting to MCP Backend...", ttl=60)
def get_server_capabilities(fastapi_base_url):
"""Fetches the lists of tools and resources from the FastAPI backend."""
try:
# Get API key from environment or generate one
api_key = os.getenv("API_SERVER_KEY")
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}

# First check if MCP is enabled and initialized
status_response = requests.get(f"{fastapi_base_url}/v1/mcp/status", headers=headers)
if status_response.status_code == 200:
status = status_response.json()
if not status.get("enabled", False):
st.warning("MCP is not enabled. Please enable it in the configuration.")
return {"error": "MCP not enabled"}, {"error": "MCP not enabled"}, {"error": "MCP not enabled"}
if not status.get("initialized", False):
st.info("MCP is enabled but not yet initialized. Please select a model first.")
return {"tools": []}, {"static": [], "dynamic": []}, {"prompts": []}

tools_response = requests.get(f"{fastapi_base_url}/v1/mcp/tools", headers=headers)
tools_response.raise_for_status()
tools = tools_response.json()

resources_response = requests.get(f"{fastapi_base_url}/v1/mcp/resources", headers=headers)
resources_response.raise_for_status()
resources = resources_response.json()

prompts_response = requests.get(f"{fastapi_base_url}/v1/mcp/prompts", headers=headers)
prompts_response.raise_for_status()
prompts = prompts_response.json()

return tools, resources, prompts
except requests.exceptions.RequestException as e:
st.error(f"Could not connect to the MCP backend at {fastapi_base_url}. Is it running? Error: {e}")
return {"tools": []}, {"static": [], "dynamic": []}, {"prompts": []}

def get_server_files():
files = ["server/mcp/server_config.json"]
try:
with open("server/mcp/server_config.json", "r") as f: config = json.load(f)
for server in config.get("mcpServers", {}).values():
script_path = server.get("args", [None])[0]
if script_path and os.path.exists(script_path): files.append(script_path)
except FileNotFoundError: st.sidebar.error("server_config.json not found!")
return list(set(files))

def display_ide_tab():
st.header("🔧 Integrated MCP Server IDE")
st.info("Edit your server configuration or scripts. Restart the launcher for changes to take effect.")
server_files = get_server_files()
selected_file = st.selectbox("Select a file to edit", options=server_files)
if selected_file:
with open(selected_file, "r") as f: file_content = f.read()
from streamlit_ace import st_ace
new_content = st_ace(value=file_content, language="python" if selected_file.endswith(".py") else "json", theme="monokai", keybinding="vscode", height=500, auto_update=True)
if st.button("Save Changes"):
with open(selected_file, "w") as f: f.write(new_content)
st.success(f"Successfully saved {selected_file}!")

def display_commands_tab(tools, resources, prompts):
st.header("📖 Discovered MCP Commands")
st.info("These commands were discovered from the MCP backend.")

if tools:
with st.expander("🛠️ Available Tools (Used automatically by the AI)", expanded=True):
# Extract just the tool names from the tools response
if "tools" in tools and isinstance(tools["tools"], list):
tool_names = [tool.get("name", tool) if isinstance(tool, dict) else tool for tool in tools["tools"]]
st.write(tool_names)
else:
st.json(tools)

if resources:
with st.expander("📦 Available Resources (Use with `@<name>` or just `<name>`)"):
st.json(resources)

if prompts:
with st.expander("📝 Available Prompts (Use with `/prompt <name>` or select in chat)"):
st.json(prompts)
18 changes: 18 additions & 0 deletions src/client/utils/st_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
import common.logging_config as logging_config
from common.schema import PromptPromptType, PromptNameType, SelectAISettings, ClientIdType

# Import the MCP initialization function
try:
from launch_server import initialize_mcp_engine_with_model
except ImportError:
initialize_mcp_engine_with_model = None

logger = logging_config.logging.getLogger("client.utils.st_common")


Expand Down Expand Up @@ -164,6 +170,8 @@ def ll_sidebar() -> None:
selected_model = state.client_settings["ll_model"]["model"]
ll_idx = list(ll_models_enabled.keys()).index(selected_model)
if not state.client_settings["selectai"]["enabled"]:
# Store the previous model to detect changes
previous_model = selected_model
selected_model = st.sidebar.selectbox(
"Chat model:",
options=list(ll_models_enabled.keys()),
Expand All @@ -172,6 +180,16 @@ def ll_sidebar() -> None:
on_change=update_client_settings("ll_model"),
disabled=state.client_settings["selectai"]["enabled"],
)

# If the model has changed, reinitialize the MCP engine
if selected_model != previous_model and initialize_mcp_engine_with_model:
try:
# Instead of creating a new event loop, we'll set a flag to indicate
# that the MCP engine needs to be reinitialized
state.mcp_needs_reinit = selected_model
logger.info(f"MCP engine marked for reinitialization with model: {selected_model}")
except Exception as e:
logger.error(f"Failed to mark MCP engine for reinitialization with model {selected_model}: {e}")

# Temperature
temperature = ll_models_enabled[selected_model]["temperature"]
Expand Down
38 changes: 38 additions & 0 deletions src/common/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,40 @@ def set_connection(self, connection: oracledb.Connection) -> None:
self._connection = connection


#####################################################
# MCP
#####################################################
class MCPModelConfig(BaseModel):
"""MCP Model Configuration"""

model_id: str = Field(..., description="Model identifier")
service_type: Literal["ollama", "openai"] = Field(..., description="AI service type")
base_url: str = Field(default="http://localhost:11434", description="Base URL for API")
api_key: Optional[str] = Field(default=None, description="API key", json_schema_extra={"sensitive": True})
enabled: bool = Field(default=True, description="Model availability status")
streaming: bool = Field(default=False, description="Enable streaming responses")
temperature: float = Field(default=1.0, description="Model temperature")
max_tokens: int = Field(default=2048, description="Maximum tokens per response")


class MCPToolConfig(BaseModel):
"""MCP Tool Configuration"""

name: str = Field(..., description="Tool name")
description: str = Field(..., description="Tool description")
parameters: dict[str, Any] = Field(..., description="Tool parameters")
enabled: bool = Field(default=True, description="Tool availability status")


class MCPSettings(BaseModel):
"""MCP Global Settings"""

models: list[MCPModelConfig] = Field(default_factory=list, description="Available MCP models")
tools: list[MCPToolConfig] = Field(default_factory=list, description="Available MCP tools")
default_model: Optional[str] = Field(default=None, description="Default model identifier")
enabled: bool = Field(default=True, description="Enable or disable MCP functionality")


#####################################################
# Models
#####################################################
Expand Down Expand Up @@ -320,6 +354,7 @@ class Configuration(BaseModel):
model_configs: Optional[list[Model]] = None
oci_configs: Optional[list[OracleCloudSettings]] = None
prompt_configs: Optional[list[Prompt]] = None
mcp_configs: Optional[list[MCPModelConfig]] = Field(default=None, description="List of MCP configurations")

def model_dump_public(self, incl_sensitive: bool = False, incl_readonly: bool = False) -> dict:
"""Remove marked fields for FastAPI Response"""
Expand Down Expand Up @@ -489,3 +524,6 @@ class EvaluationReport(Evaluation):
TestSetsIdType = TestSets.__annotations__["tid"]
TestSetsNameType = TestSets.__annotations__["name"]
TestSetDateType = TestSets.__annotations__["created"]
MCPModelIdType = MCPModelConfig.__annotations__["model_id"]
MCPServiceType = MCPModelConfig.__annotations__["service_type"]
MCPToolNameType = MCPToolConfig.__annotations__["name"]
4 changes: 4 additions & 0 deletions src/launch_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def main() -> None:
state.disabled["model_cfg"] = os.environ.get("DISABLE_MODEL_CFG", "false").lower() == "true"
state.disabled["oci_cfg"] = os.environ.get("DISABLE_OCI_CFG", "false").lower() == "true"
state.disabled["settings"] = os.environ.get("DISABLE_SETTINGS", "false").lower() == "true"
state.disabled["mcp_cfg"] = os.environ.get("DISABLE_MCP_CFG", "false").lower() == "true"

# Left Hand Side - Navigation
chatbot = st.Page("client/content/chatbot.py", title="ChatBot", icon="💬", default=True)
Expand Down Expand Up @@ -166,6 +167,9 @@ def main() -> None:
# When we get here, if there's nothing in "Configuration" delete it
if not navigation["Configuration"]:
del navigation["Configuration"]
if not state.disabled["mcp_cfg"]:
mcp_config = st.Page("client/content/config/mcp_servers.py", title="MCP Servers", icon="💾")
navigation["Configuration"].append(mcp_config)

pg = st.navigation(navigation, position="sidebar", expanded=False)
pg.run()
Expand Down
Loading