From 69be7f20296bd3786bccab490f3b696f13e89159 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 22:38:57 +0530 Subject: [PATCH] SimpleSallap:SimpleMCP: Use ToolManager for some of needed logics Build the list of tool calls Trap some of the MCP post json based requests and map to related handlers. Inturn implement the tool call execution handler. Add some helper dataclasses wrt expected MCP response structure TOTHINK: For now maintain id has a string and not int, with idea to map it directly to callid wrt tool call handshake by ai model. TOCHECK: For now suffle the order of fields wrt jsonrpc and type wrt MCP response related structures, assuming the order shouldnt matter. Need to cross check. --- .../public_simplechat/docs/changelog.md | 6 ++ .../public_simplechat/local.tools/config.py | 2 + .../local.tools/simplemcp.py | 86 ++++++++++--------- .../public_simplechat/local.tools/toolcall.py | 19 ++-- 4 files changed, 67 insertions(+), 46 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index e67233bca8..644e76ee03 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -317,6 +317,12 @@ Chat Session specific settings * enable multi threaded ssl and client request handling, so that rogue clients cant mount simple DoS by opening connection and then missing in action. * switch to a Dicty DataClass based Config with better type validation and usage, instead of literal dict++ +* ToolCall, ToolManager and related classes based flow wrt the tool calls. + * all existing tool calls duplicated and updated to support and build on this new flow. +* Initial skeleton towards SimpleMCP, a post and json based handshake flow, so that the tool calls supported + through SimpleProxy can be exposed through a MCP standardish mechanism. + * can allow others beyond AnveshikaSallap client to use the corresponding tool calls + * can allow AnveshikaSallap client to support other MCP servers and their exposed tool calls in future. ## ToDo diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index b6c7a11bab..1979db015a 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -9,6 +9,7 @@ import ssl import sys import urlvalidator as mUV import debug as mDebug +import toolcall as mTC gConfigNeeded = [ 'acl.schemes', 'acl.domains', 'sec.bearerAuth' ] @@ -75,6 +76,7 @@ class Op(DictyDataclassMixin): debug: bool = False server: http.server.ThreadingHTTPServer|None = None sslContext: ssl.SSLContext|None = None + toolManager: mTC.ToolManager|None = None bearerTransformed: str = "" bearerTransformedYear: str = "" diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index b5c497eb31..2d1e098a7f 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -18,7 +18,7 @@ import time import ssl import traceback import json -from typing import Callable +from typing import Any import tcpdf as mTCPdf import tcweb as mTCWeb import toolcall as mTC @@ -28,14 +28,6 @@ import config as mConfig gMe = mConfig.Config() -gAllowedCalls = { - "xmlfiltered": [], - "htmltext": [], - "urlraw": [], - "pdftext": [ "pypdf" ] - } - - def bearer_transform(): """ Transform the raw bearer token to the network handshaked token, @@ -94,18 +86,44 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): return mTC.TCOutResponse(False, 400, "WARN:Invalid auth") return mTC.TCOutResponse(True, 200, "Auth Ok") - def auth_and_run(self, pr:urllib.parse.ParseResult, handler:Callable[['ProxyHandler', urllib.parse.ParseResult], None]): + def mcp_toolscall(self, oRPC: Any): """ If authorisation is ok for the request, run the specified handler. """ - acGot = self.auth_check() - if not acGot.callOk: - self.send_error(acGot.statusCode, acGot.statusMsg) + try: + if not gMe.op.toolManager: + raise RuntimeError("DBUG:PH:TCRun:ToolManager uninitialised") + resp = gMe.op.toolManager.tc_handle(oRPC["id"], oRPC["params"]["name"], oRPC["params"]["arguments"], self.headers) + if not resp.response.callOk: + self.send_error(resp.response.statusCode, resp.response.statusMsg) + return + tcresp = mTC.MCPToolCallResponse( + resp.tcid, + resp.name, + mTC.MCPTCRResult([ + mTC.MCPTCRContentText(resp.response.contentData.decode('utf-8')) + ]) + ) + self.send_response(resp.response.statusCode, resp.response.statusMsg) + self.send_header('Content-Type', "application/json") + # Add CORS for browser fetch, just in case + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(json.dumps(tcresp).encode('utf-8')) + except Exception as e: + self.send_error(400, f"ERRR:PH:{e}") + + def mcp_toolslist(self): + + pass + + def mcp_run(self, oRPC: Any): + if oRPC["method"] == "tools/call": + self.mcp_toolscall(oRPC) + elif oRPC["method"] == "tools/list": + self.mcp_toolslist() else: - try: - handler(self, pr) - except Exception as e: - self.send_error(400, f"ERRR:ProxyHandler:{e}") + self.send_error(400, f"ERRR:PH:MCP:Unknown") def _do_POST(self): """ @@ -124,7 +142,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): if len(body) == gMe.nw.maxReadBytes: self.send_error(400, f"WARN:RequestOverflow:{pr.path}") oRPC = json.loads(body) - + self.mcp_run(oRPC) def do_POST(self): """ @@ -159,33 +177,18 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): return super().handle() -def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): +def setup_toolmanager(): """ Handle requests to aum path, which is used in a simple way to verify that one is communicating with this proxy server """ - import importlib - queryParams = urllib.parse.parse_qs(pr.query) - url = queryParams['url'] - print(f"DBUG:HandleAUM:Url:{url}") - url = url[0] - if (not url) or (len(url) == 0): - ph.send_error(400, f"WARN:HandleAUM:MissingUrl/UnknownQuery?!") - return - urlParts = url.split('.',1) - if gAllowedCalls.get(urlParts[0], None) == None: - ph.send_error(403, f"WARN:HandleAUM:Forbidden:{urlParts[0]}") - return - for dep in gAllowedCalls[urlParts[0]]: - try: - importlib.import_module(dep) - except ImportError as exc: - ph.send_error(400, f"WARN:HandleAUM:{urlParts[0]}:Support module [{dep}] missing or has issues") - return - print(f"INFO:HandleAUM:Availability ok for:{urlParts[0]}") - ph.send_response_only(200, "bharatavarshe") - ph.send_header('Access-Control-Allow-Origin', '*') - ph.end_headers() + gMe.op.toolManager = mTC.ToolManager() + if mTCWeb.ok(): + gMe.op.toolManager.tc_add("fetch_url_raw", mTCWeb.TCUrlRaw("fetch_url_raw")) + gMe.op.toolManager.tc_add("fetch_html_text", mTCWeb.TCHtmlText("fetch_html_text")) + gMe.op.toolManager.tc_add("fetch_xml_filtered", mTCWeb.TCXmlFiltered("fetch_xml_filtered")) + if mTCPdf.ok(): + gMe.op.toolManager.tc_add("fetch_pdf_text", mTCPdf.TCPdfText("fetch_pdf_text")) def setup_server(): @@ -228,4 +231,5 @@ def run(): if __name__ == "__main__": gMe.process_args(sys.argv) + setup_toolmanager() run() diff --git a/tools/server/public_simplechat/local.tools/toolcall.py b/tools/server/public_simplechat/local.tools/toolcall.py index 3b3c297ff1..39d40820eb 100644 --- a/tools/server/public_simplechat/local.tools/toolcall.py +++ b/tools/server/public_simplechat/local.tools/toolcall.py @@ -78,12 +78,21 @@ class ToolCallResponseEx(): name: str response: TCOutResponse +@dataclass(frozen=True) +class MCPTCRContentText: + text: str + type: str = "text" + @dataclass -class ToolCallResponse(): - status: bool - tcid: str +class MCPTCRResult: + content: list[MCPTCRContentText] + +@dataclass +class MCPToolCallResponse: + id: str name: str - content: str = "" + result: MCPTCRResult + jsonrpc: str = "2.0" HttpHeaders: TypeAlias = dict[str, str] | email.message.Message[str, str] @@ -116,7 +125,7 @@ class ToolManager(): for tcName in self.toolcalls.keys(): oMeta[tcName] = self.toolcalls[tcName].meta() - def tc_handle(self, tcName: str, callId: str, tcArgs: TCInArgs, inHeaders: HttpHeaders) -> ToolCallResponseEx: + def tc_handle(self, callId: str, tcName: str, tcArgs: TCInArgs, inHeaders: HttpHeaders) -> ToolCallResponseEx: try: response = self.toolcalls[tcName].tc_handle(tcArgs, inHeaders) return ToolCallResponseEx(callId, tcName, response)