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.
This commit is contained in:
hanishkvc 2025-12-06 22:38:57 +05:30
parent 8700d522a5
commit 69be7f2029
4 changed files with 67 additions and 46 deletions

View File

@ -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

View File

@ -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 = ""

View File

@ -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()

View File

@ -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)