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)