,
+ } satisfies LoadSetPair<[number, ToolStructure]>,
];
// Execute all loaders in parallel and wait for them all to complete
@@ -260,6 +282,7 @@ export const AppContextProvider = ({ children }: PropsWithChildren) => {
logout,
userLoginName,
isUserLoginLoading,
+ tools,
}}
>
{children}
diff --git a/client/src/declarations.d.ts b/client/src/declarations.d.ts
index 778550a..c5beea9 100644
--- a/client/src/declarations.d.ts
+++ b/client/src/declarations.d.ts
@@ -4,6 +4,7 @@ interface ImportMetaEnv {
readonly APP: string;
readonly CLIENT_ACCESS_KEY: string;
readonly CLIENT_SECRET_KEY: string;
+ readonly HOME_PAGE_ENABLED: string;
// more env variables...
}
diff --git a/client/src/index.html b/client/src/index.html
index aeebc91..1607703 100644
--- a/client/src/index.html
+++ b/client/src/index.html
@@ -1,12 +1,11 @@
-
+
-
- SEMOSS Template
-
-
+
+ Sample MCP
+
+
-
diff --git a/client/src/pages/HomePage.tsx b/client/src/pages/HomePage.tsx
index c3bbf21..087d284 100644
--- a/client/src/pages/HomePage.tsx
+++ b/client/src/pages/HomePage.tsx
@@ -1,10 +1,46 @@
-import { ExampleComponent } from "@/components";
+/** biome-ignore-all lint/suspicious/noExplicitAny: TODO */
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { useAppContext } from "@/contexts";
-/**
- * Renders the home page, currently displaying an example component.
- *
- * @component
- */
export const HomePage = () => {
- return ;
+ const { tools } = useAppContext();
+ // Function to format page names for display
+ const formatPageName = (pageName: string) => {
+ return pageName
+ .replace(/_/g, " ") // Replace underscores with spaces
+ .split(" ")
+ .map(
+ (word) =>
+ word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
+ ) // Capitalize first letter of each word
+ .join(" ");
+ };
+
+ return (
+
+
+
+
+ {tools?.map((tool) => {
+ const name = tool?.name || "";
+ return (
+
+ );
+ })}
+
+
+
+
+ );
};
diff --git a/client/src/pages/Router.tsx b/client/src/pages/Router.tsx
index 3751191..2abf7f1 100644
--- a/client/src/pages/Router.tsx
+++ b/client/src/pages/Router.tsx
@@ -1,9 +1,17 @@
-import { createHashRouter, Navigate, RouterProvider } from "react-router-dom";
+import {
+ createHashRouter,
+ Navigate,
+ RouterProvider,
+ useParams,
+} from "react-router-dom";
+import { DefaultToolView, LoadingScreen, PageWrapper } from "@/components";
+import { useAppContext } from "@/contexts";
import { ROUTE_PATH_LOGIN_PAGE } from "@/routes.constants";
import { ErrorPage } from "./ErrorPage";
import { HomePage } from "./HomePage";
import { LoginPage } from "./LoginPage";
import { AuthorizedLayout, InitializedLayout } from "./layouts";
+import { MCPLayout } from "./layouts/MCPLayout";
const router = createHashRouter([
{
@@ -19,9 +27,37 @@ const router = createHashRouter([
ErrorBoundary: ErrorPage,
children: [
{
- // If the path is empty, use the home page
- index: true,
- Component: HomePage,
+ // MCPLayout handles routing logic - redirects to tool or shows children
+ Component: MCPLayout,
+ children: [
+ {
+ // Home page - shows list of available tools
+ index: true,
+ Component: HomePage,
+ },
+ {
+ // Route with page parameter - renders different pages based on the "page" param
+ path: ":pageName",
+ Component: () => {
+ const { isAppDataLoading } =
+ useAppContext();
+ const { pageName } = useParams<{
+ pageName: string;
+ }>();
+ if (isAppDataLoading || !pageName) {
+ return ;
+ }
+ return import.meta.env.HOME_PAGE_ENABLED ===
+ "true" ? (
+
+
+
+ ) : (
+
+ );
+ },
+ },
+ ],
},
// {
// // Example of a new page
diff --git a/client/src/pages/layouts/InitializedLayout.tsx b/client/src/pages/layouts/InitializedLayout.tsx
index 9b948a6..ddf75b7 100644
--- a/client/src/pages/layouts/InitializedLayout.tsx
+++ b/client/src/pages/layouts/InitializedLayout.tsx
@@ -1,6 +1,6 @@
import { useInsight } from "@semoss/sdk/react";
import { Outlet } from "react-router-dom";
-import { LoadingScreen, MainNavigation } from "@/components";
+import { LoadingScreen } from "@/components";
import { ErrorPage } from "../ErrorPage";
/**
@@ -17,7 +17,7 @@ export const InitializedLayout = () => {
return (
{/* Allow users to navigate around the app */}
-
+ {/* */}
{isInitialized ? (
// If initialized, set up padding and scroll
diff --git a/client/src/pages/layouts/MCPLayout.tsx b/client/src/pages/layouts/MCPLayout.tsx
new file mode 100644
index 0000000..c0394db
--- /dev/null
+++ b/client/src/pages/layouts/MCPLayout.tsx
@@ -0,0 +1,48 @@
+import { Navigate, Outlet, useLocation } from "react-router-dom";
+import { useInsight } from "@semoss/sdk/react";
+import { LoadingScreen } from "@/components";
+import { useAppContext } from "@/contexts";
+
+// Function to process tool name by removing content before first underscore
+const getProcessedToolName = (toolName: string) => {
+ const underscoreIndex = toolName.indexOf("_");
+ return underscoreIndex !== -1
+ ? toolName.substring(underscoreIndex + 1)
+ : toolName;
+};
+
+/**
+ * Sends users to the login page if they are not authorized, shows a loading screen while app data is loading, otherwise renders the child components.
+ *
+ * @component
+ */
+export const MCPLayout = () => {
+ // Get the curent route, so that if we are trying to log the user in, we can take them to where they were trying to go
+ const { pathname } = useLocation();
+ const { tool } = useInsight();
+ const { isAppDataLoading } = useAppContext();
+
+ // If the app data is still loading, show a loading screen
+ if (isAppDataLoading) {
+ return ;
+ }
+
+ // If we have a tool, check if we need to redirect
+ if (tool?.name) {
+ const processedToolName = getProcessedToolName(tool.name);
+ const expectedPath = `/${processedToolName}`;
+
+ // If we're at root, redirect to the tool page
+ if (pathname === "/") {
+ return ;
+ }
+
+ // If we're on a different tool page, redirect to the correct one
+ if (pathname !== expectedPath) {
+ return ;
+ }
+ }
+
+ // Render child routes (HomePage or tool pages)
+ return ;
+};
diff --git a/client/src/pages/layouts/index.ts b/client/src/pages/layouts/index.ts
index 407ae55..44a8a53 100644
--- a/client/src/pages/layouts/index.ts
+++ b/client/src/pages/layouts/index.ts
@@ -1,2 +1,3 @@
export * from "./AuthorizedLayout";
export * from "./InitializedLayout";
+export * from "./MCPLayout";
diff --git a/client/src/routes.constants.ts b/client/src/routes.constants.ts
deleted file mode 100644
index e5c32df..0000000
--- a/client/src/routes.constants.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const ROUTE_PATH_LOGIN_PAGE = "login";
diff --git a/client/src/routes.constants.tsx b/client/src/routes.constants.tsx
new file mode 100644
index 0000000..576c9b7
--- /dev/null
+++ b/client/src/routes.constants.tsx
@@ -0,0 +1,14 @@
+import { GetStockHistory } from "./components/mcp/GetStockHistory";
+import { GetStockPrice } from "./components/mcp/GetStockPrice";
+
+export const ROUTE_PATH_LOGIN_PAGE = "login";
+
+/**
+ * Page/component mapping. Keys much match tool name to render correctly, otherwise uses a default json view
+ *
+ */
+export const PAGE_TYPES = {
+ get_stock_price: ,
+ get_stock_history: ,
+ // stock_resource: "stock_resource", //
+} as const;
diff --git a/client/src/types.d.ts b/client/src/types.d.ts
index e69de29..e97060e 100644
--- a/client/src/types.d.ts
+++ b/client/src/types.d.ts
@@ -0,0 +1,51 @@
+export interface MCPTool {
+ name: string;
+ description?: string;
+ inputSchema?: {
+ type: "object";
+ title: string;
+ properties?: {
+ [key: string]: {
+ type: string;
+ description?: string;
+ enum?: string[];
+ items?: unknown;
+ minimum?: number;
+ maximum?: number;
+ minLength?: number;
+ maxLength?: number;
+ pattern?: string;
+ format?: string;
+ default?: unknown;
+ };
+ };
+ required?: string[];
+ additionalProperties?: boolean;
+ };
+}
+
+export interface Tool extends MCPTool {
+ name: string;
+ description: string;
+ _meta: { generated_on: string };
+ title: string;
+}
+
+export interface ToolStructure {
+ _meta: {
+ SMSS_PROJECT_NAME: string;
+ SMSS_PROJECT_ID: string;
+ SMSS_ENGINE_NAME: string;
+ SMSS_ENGINE_TYPE: string;
+ SMSS_ENGINE_ID: string;
+ };
+ tools: Tool[];
+}
+
+export interface ToolResponse {
+ id: string;
+ message: string;
+ name: string;
+ type: string;
+ parameters: Record;
+}
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 86f92c6..3d82393 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -8,6 +8,7 @@ export default defineConfig(({ mode }) => {
ENDPOINT: string;
MODULE: string;
APP: string;
+ HOME_PAGE_ENABLED: string;
};
return {
@@ -24,6 +25,9 @@ export default defineConfig(({ mode }) => {
"import.meta.env.ENDPOINT": JSON.stringify(env.ENDPOINT),
"import.meta.env.MODULE": JSON.stringify(env.MODULE),
"import.meta.env.APP": JSON.stringify(env.APP),
+ "import.meta.env.HOME_PAGE_ENABLED": JSON.stringify(
+ env.HOME_PAGE_ENABLED,
+ ),
},
server: {
proxy: {
diff --git a/mcp/py_mcp.json b/mcp/py_mcp.json
new file mode 100644
index 0000000..396cd6e
--- /dev/null
+++ b/mcp/py_mcp.json
@@ -0,0 +1,93 @@
+{
+ "_meta": {
+ "last_modified_date": "2025-10-27",
+ "file_last_modified_date": "2025-10-27",
+ "source_file": "C:/Users/kzifrony/Documents/ADMSS/workspace/Semoss/project/MCP Sample__b45a88db-7a31-4e6c-a1c7-60846426f3b1/app_root/version/assets/py/smss_driver.py"
+ },
+ "tools": [
+ {
+ "name": "get_stock_price",
+ "title": "Get Stock Price",
+ "description": "Retrieve the current stock price for the given ticker symbol.\nReturns the latest closing price as a float.",
+ "inputSchema": {
+ "properties": {
+ "symbol": {
+ "title": "Symbol",
+ "type": "string"
+ }
+ },
+ "required": ["symbol"],
+ "title": "Get Stock Price Arguments",
+ "type": "object"
+ },
+ "_meta": {
+ "generated_on": "2025-10-27"
+ }
+ },
+ {
+ "name": "stock_resource",
+ "title": "Stock Resource",
+ "description": "Expose stock price data as a resource.\nReturns a formatted string with the current stock price for the given symbol.",
+ "inputSchema": {
+ "properties": {
+ "symbol": {
+ "title": "Symbol",
+ "type": "string"
+ }
+ },
+ "required": ["symbol"],
+ "title": "Stock Resource Arguments",
+ "type": "object"
+ },
+ "_meta": {
+ "generated_on": "2025-10-27"
+ }
+ },
+ {
+ "name": "get_stock_history",
+ "title": "Get Stock History",
+ "description": "Retrieve historical data for a stock given a ticker symbol and a period.\nReturns the historical data as a CSV formatted string.\n\nParameters:\n symbol: The stock ticker symbol.\n period: The period over which to retrieve historical data (e.g., '1mo', '3mo', '1y').",
+ "inputSchema": {
+ "properties": {
+ "symbol": {
+ "title": "Symbol",
+ "type": "string"
+ },
+ "period": {
+ "title": "Period",
+ "type": "string"
+ }
+ },
+ "required": ["symbol", "period"],
+ "title": "Get Stock History Arguments",
+ "type": "object"
+ },
+ "_meta": {
+ "generated_on": "2025-10-27"
+ }
+ },
+ {
+ "name": "compare_stocks",
+ "title": "Compare Stocks",
+ "description": "Compare the current stock prices of two ticker symbols.\nReturns a formatted message comparing the two stock prices.\n\nParameters:\n symbol1: The first stock ticker symbol.\n symbol2: The second stock ticker symbol.",
+ "inputSchema": {
+ "properties": {
+ "symbol1": {
+ "title": "Symbol1",
+ "type": "string"
+ },
+ "symbol2": {
+ "title": "Symbol2",
+ "type": "string"
+ }
+ },
+ "required": ["symbol1", "symbol2"],
+ "title": "Compare Stocks Arguments",
+ "type": "object"
+ },
+ "_meta": {
+ "generated_on": "2025-10-27"
+ }
+ }
+ ]
+}
diff --git a/py/smss_driver.py b/py/smss_driver.py
new file mode 100644
index 0000000..06ef5cf
--- /dev/null
+++ b/py/smss_driver.py
@@ -0,0 +1,86 @@
+import yfinance as yf
+from smssutil import mcp_execution
+
+
+@mcp_execution("auto")
+def get_stock_price(symbol: str) -> float:
+ """
+ Retrieve the current stock price for the given ticker symbol.
+ Returns the latest closing price as a float.
+ """
+ try:
+ ## GCA / AScend with API
+ ticker = yf.Ticker(symbol)
+ # Get today's historical data; may return empty if market is closed or symbol is invalid.
+ data = ticker.history(period="1d")
+ if not data.empty:
+ # Use the last closing price from today's data
+ price = data["Close"].iloc[-1]
+ return float(price)
+ else:
+ # As a fallback, try using the regular market price from the ticker info
+ info = ticker.info
+ price = info.get("regularMarketPrice", None)
+ if price is not None:
+ return float(price)
+ else:
+ return -1.0 # Indicate failure
+ except Exception:
+ # Return -1.0 to indicate an error occurred when fetching the stock price
+ return -1.0
+
+
+@mcp_execution("ask")
+def stock_resource(symbol: str) -> str:
+ """
+ Expose stock price data as a resource.
+ Returns a formatted string with the current stock price for the given symbol.
+ """
+ price = get_stock_price(symbol)
+ if price < 0:
+ return f"Error: Could not retrieve price for symbol '{symbol}'."
+ return f"The current price of '{symbol}' is ${price:.2f}."
+
+
+@mcp_execution("disabled")
+def get_stock_history(symbol: str, period: str = "1mo") -> str:
+ """
+ Retrieve historical data for a stock given a ticker symbol and a period.
+ Returns the historical data as a CSV formatted string.
+
+ Parameters:
+ symbol: The stock ticker symbol.
+ period: The period over which to retrieve historical data (e.g., '1mo', '3mo', '1y').
+ """
+ try:
+ ticker = yf.Ticker(symbol)
+ data = ticker.history(period=period)
+ if data.empty:
+ return f"No historical data found for symbol '{symbol}' with period '{period}'."
+ # Convert the DataFrame to a CSV formatted string
+ csv_data = data.to_csv()
+ return csv_data
+ except Exception as e:
+ return f"Error fetching historical data: {str(e)}"
+
+
+def compare_stocks(symbol1: str, symbol2: str) -> str:
+ """
+ Compare the current stock prices of two ticker symbols.
+ Returns a formatted message comparing the two stock prices.
+
+ Parameters:
+ symbol1: The first stock ticker symbol.
+ symbol2: The second stock ticker symbol.
+ """
+ price1 = get_stock_price(symbol1)
+ price2 = get_stock_price(symbol2)
+ if price1 < 0 or price2 < 0:
+ return f"Error: Could not retrieve data for comparison of '{symbol1}' and '{symbol2}'."
+ if price1 > price2:
+ result = f"{symbol1} (${price1:.2f}) is higher than {symbol2} (${price2:.2f})."
+ elif price1 < price2:
+ result = f"{symbol1} (${price1:.2f}) is lower than {symbol2} (${price2:.2f})."
+ else:
+ result = f"Both {symbol1} and {symbol2} have the same price (${price1:.2f})."
+ return result