@@ -149,6 +149,55 @@ def _api_get(path: str) -> Any:
149149 "required" : ["training" , "test" ],
150150 },
151151 },
152+ {
153+ "name" : "waveguard_scan_timeseries" ,
154+ "description" : (
155+ "Detect anomalies in time-series data using GPU-accelerated wave "
156+ "physics simulation. Send a flat array of numeric values and a "
157+ "window size. The tool automatically creates overlapping windows, "
158+ "uses the first N as training (normal baseline), and scores the "
159+ "remaining windows as test samples. Returns per-window anomaly "
160+ "scores, confidence, and p-values.\n \n "
161+ "Example: send 100 CPU-usage readings with window_size=10. "
162+ "The first 5 windows become training, the rest are tested."
163+ ),
164+ "inputSchema" : {
165+ "type" : "object" ,
166+ "properties" : {
167+ "data" : {
168+ "type" : "array" ,
169+ "items" : {"type" : "number" },
170+ "description" : (
171+ "Flat array of numeric time-series values in "
172+ "chronological order."
173+ ),
174+ "minItems" : 4 ,
175+ },
176+ "window_size" : {
177+ "type" : "integer" ,
178+ "description" : (
179+ "Number of data points per window (default: 10). "
180+ "Smaller windows = finer resolution."
181+ ),
182+ },
183+ "test_windows" : {
184+ "type" : "integer" ,
185+ "description" : (
186+ "Number of trailing windows to test (default: auto, "
187+ "uses last ~40%% of windows)."
188+ ),
189+ },
190+ "sensitivity" : {
191+ "type" : "number" ,
192+ "description" : (
193+ "Anomaly threshold multiplier (default: 2.0). Lower = "
194+ "more sensitive. Range: 0.5 to 5.0."
195+ ),
196+ },
197+ },
198+ "required" : ["data" ],
199+ },
200+ },
152201 {
153202 "name" : "waveguard_health" ,
154203 "description" : (
@@ -169,6 +218,71 @@ def _api_get(path: str) -> Any:
169218# ═══════════════════════════════════════════════════════════════════════════
170219
171220
221+ def _execute_timeseries (arguments : dict ) -> dict :
222+ """Sliding-window timeseries scan via the /v1/scan endpoint."""
223+ data = arguments ["data" ]
224+ window = int (arguments .get ("window_size" , 10 ))
225+ sensitivity = arguments .get ("sensitivity" , 2.0 )
226+
227+ # Build windows
228+ windows = [data [i : i + window ] for i in range (0 , len (data ) - window + 1 )]
229+ if len (windows ) < 3 :
230+ return {
231+ "content" : [
232+ {
233+ "type" : "text" ,
234+ "text" : (
235+ f"Not enough data: { len (data )} points with "
236+ f"window_size={ window } gives { len (windows )} windows "
237+ f"(need at least 3)."
238+ ),
239+ }
240+ ],
241+ "isError" : True ,
242+ }
243+
244+ # Split into training / test
245+ test_count = arguments .get ("test_windows" )
246+ if test_count is None :
247+ test_count = max (1 , len (windows ) * 2 // 5 )
248+ test_count = min (test_count , len (windows ) - 2 )
249+
250+ training = windows [: len (windows ) - test_count ]
251+ test = windows [len (windows ) - test_count :]
252+
253+ body : dict = {
254+ "training" : training ,
255+ "test" : test ,
256+ "encoder_type" : "timeseries" ,
257+ "sensitivity" : sensitivity ,
258+ }
259+ result = _api_post ("/v1/scan" , body )
260+
261+ # Summarise
262+ lines = [
263+ f"Time-series scan: { len (windows )} windows "
264+ f"(window_size={ window } , { len (training )} train, { len (test )} test)" ,
265+ "" ,
266+ ]
267+ for i , r in enumerate (result .get ("results" , [])):
268+ idx = len (training ) + i
269+ is_anom = r .get ("is_anomaly" , False )
270+ conf = r .get ("confidence" , 0 )
271+ pval = r .get ("p_value" , 1.0 )
272+ marker = "ANOMALY" if is_anom else "Normal"
273+ lines .append (
274+ f" Window { idx } : { marker } (confidence: { conf :.0%} , "
275+ f"p-value: { pval :.4f} )"
276+ )
277+ summary = "\n " .join (lines )
278+ return {
279+ "content" : [
280+ {"type" : "text" , "text" : summary },
281+ {"type" : "text" , "text" : json .dumps (result , indent = 2 )},
282+ ]
283+ }
284+
285+
172286def execute_tool (name : str , arguments : dict ) -> dict :
173287 """Execute an MCP tool and return the result."""
174288 try :
@@ -221,6 +335,9 @@ def execute_tool(name: str, arguments: dict) -> dict:
221335 ]
222336 }
223337
338+ elif name == "waveguard_scan_timeseries" :
339+ return _execute_timeseries (arguments )
340+
224341 elif name == "waveguard_health" :
225342 result = _api_get ("/v1/health" )
226343 status = (
@@ -254,7 +371,7 @@ class MCPStdioServer:
254371 def __init__ (self ) -> None :
255372 self .server_info = {
256373 "name" : "waveguard" ,
257- "version" : "2.0 .0" ,
374+ "version" : "2.1 .0" ,
258375 }
259376
260377 def handle_message (self , msg : dict ) -> Optional [dict ]:
@@ -318,7 +435,7 @@ def handle_message(self, msg: dict) -> Optional[dict]:
318435 def run_stdio (self ) -> None :
319436 """Run the MCP server on stdin/stdout."""
320437 sys .stderr .write (
321- f"WaveGuard MCP server v2.0 .0 started (API: { API_URL } )\n "
438+ f"WaveGuard MCP server v2.1 .0 started (API: { API_URL } )\n "
322439 )
323440 sys .stderr .flush ()
324441
@@ -353,7 +470,7 @@ def run_http_server(port: int = 3001) -> None:
353470 print ("HTTP transport requires: pip install fastapi uvicorn" )
354471 sys .exit (1 )
355472
356- mcp_app = FA (title = "WaveGuard MCP Server" , version = "2.0 .0" )
473+ mcp_app = FA (title = "WaveGuard MCP Server" , version = "2.1 .0" )
357474 server = MCPStdioServer ()
358475
359476 @mcp_app .post ("/mcp" )
@@ -364,7 +481,7 @@ async def mcp_endpoint(request: dict) -> dict: # type: ignore[type-arg]
364481 async def mcp_tools () -> dict : # type: ignore[type-arg]
365482 return {"tools" : TOOLS }
366483
367- print (f"WaveGuard MCP HTTP server v2.0 .0 on port { port } " )
484+ print (f"WaveGuard MCP HTTP server v2.1 .0 on port { port } " )
368485 uvicorn .run (mcp_app , host = "0.0.0.0" , port = port )
369486
370487
@@ -374,7 +491,7 @@ async def mcp_tools() -> dict: # type: ignore[type-arg]
374491
375492if __name__ == "__main__" :
376493 parser = argparse .ArgumentParser (
377- description = "WaveGuard MCP Server v2.0 .0"
494+ description = "WaveGuard MCP Server v2.1 .0"
378495 )
379496 parser .add_argument (
380497 "--http" ,
0 commit comments