232 lines
7.8 KiB
Python
232 lines
7.8 KiB
Python
# A simple mcp server with a bunch of bundled tool calls
|
|
# by Humans for All
|
|
#
|
|
# Listens on the specified port (defaults to squids 3128)
|
|
# * return the supported tool calls meta data when requested
|
|
# * execute the requested tool call and return the results
|
|
# * any request to aum path is used to respond with a predefined text response
|
|
# which can help identify this server, in a simple way.
|
|
#
|
|
# Expects a Bearer authorization line in the http header of the requests got.
|
|
#
|
|
|
|
|
|
import sys
|
|
import http.server
|
|
import urllib.parse
|
|
import time
|
|
import ssl
|
|
import traceback
|
|
import json
|
|
from typing import Callable
|
|
import tcpdf as mTCPdf
|
|
import tcweb as mTCWeb
|
|
import toolcall as mTC
|
|
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,
|
|
if and when needed.
|
|
"""
|
|
global gMe
|
|
year = str(time.gmtime().tm_year)
|
|
if gMe.op.bearerTransformedYear == year:
|
|
return
|
|
import hashlib
|
|
s256 = hashlib.sha256(year.encode('utf-8'))
|
|
s256.update(gMe.sec.bearerAuth.encode('utf-8'))
|
|
gMe.op.bearerTransformed = s256.hexdigest()
|
|
gMe.op.bearerTransformedYear = year
|
|
|
|
|
|
class ProxyHandler(http.server.BaseHTTPRequestHandler):
|
|
"""
|
|
Implements the logic for handling requests sent to this server.
|
|
"""
|
|
|
|
def send_headers_common(self):
|
|
"""
|
|
Common headers to include in responses from this server
|
|
"""
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS')
|
|
self.send_header('Access-Control-Allow-Headers', '*')
|
|
self.end_headers()
|
|
|
|
def send_error(self, code: int, message: str | None = None, explain: str | None = None) -> None:
|
|
"""
|
|
Overrides the SendError helper
|
|
so that the common headers mentioned above can get added to them
|
|
else CORS failure will be triggered by the browser on fetch from browser.
|
|
"""
|
|
print(f"WARN:PH:SendError:{code}:{message}")
|
|
self.send_response(code, message)
|
|
self.send_headers_common()
|
|
|
|
def auth_check(self):
|
|
"""
|
|
Simple Bearer authorization
|
|
ALERT: For multiple reasons, this is a very insecure implementation.
|
|
"""
|
|
bearer_transform()
|
|
authline = self.headers['Authorization']
|
|
if authline == None:
|
|
return mTC.TCOutResponse(False, 400, "WARN:No auth line")
|
|
authlineA = authline.strip().split(' ')
|
|
if len(authlineA) != 2:
|
|
return mTC.TCOutResponse(False, 400, "WARN:Invalid auth line")
|
|
if authlineA[0] != 'Bearer':
|
|
return mTC.TCOutResponse(False, 400, "WARN:Invalid auth type")
|
|
if authlineA[1] != gMe.op.bearerTransformed:
|
|
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]):
|
|
"""
|
|
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)
|
|
else:
|
|
try:
|
|
handler(self, pr)
|
|
except Exception as e:
|
|
self.send_error(400, f"ERRR:ProxyHandler:{e}")
|
|
|
|
def _do_POST(self):
|
|
"""
|
|
Handle POST requests
|
|
"""
|
|
print(f"DBUG:PH:Post:{self.address_string()}:{self.path}")
|
|
print(f"DBUG:PH:Post:Headers:{self.headers}")
|
|
acGot = self.auth_check()
|
|
if not acGot.callOk:
|
|
self.send_error(acGot.statusCode, acGot.statusMsg)
|
|
pr = urllib.parse.urlparse(self.path)
|
|
print(f"DBUG:ProxyHandler:GET:{pr}")
|
|
if pr.path != '/mcp':
|
|
self.send_error(400, f"WARN:UnknownPath:{pr.path}")
|
|
body = self.rfile.read(gMe.nw.maxReadBytes)
|
|
if len(body) == gMe.nw.maxReadBytes:
|
|
self.send_error(400, f"WARN:RequestOverflow:{pr.path}")
|
|
oRPC = json.loads(body)
|
|
|
|
|
|
def do_POST(self):
|
|
"""
|
|
Catch all / trap any exceptions wrt actual post based request handling.
|
|
"""
|
|
try:
|
|
self._do_POST()
|
|
except:
|
|
print(f"ERRR:PH:ThePOST:{traceback.format_exception_only(sys.exception())}")
|
|
self.send_error(500, f"ERRR: handling request")
|
|
|
|
def do_OPTIONS(self):
|
|
"""
|
|
Handle OPTIONS for CORS preflights (just in case from browser)
|
|
"""
|
|
print(f"DBUG:ProxyHandler:OPTIONS:{self.path}")
|
|
self.send_response(200)
|
|
self.send_headers_common()
|
|
|
|
def handle(self) -> None:
|
|
"""
|
|
Helps handle ssl setup in the client specific thread, if in https mode
|
|
"""
|
|
print(f"\n\n\nDBUG:ProxyHandler:Handle:RequestFrom:{self.client_address}")
|
|
try:
|
|
if (gMe.op.sslContext):
|
|
self.request = gMe.op.sslContext.wrap_socket(self.request, server_side=True)
|
|
self.setup()
|
|
except:
|
|
print(f"ERRR:ProxyHandler:SSLHS:{traceback.format_exception_only(sys.exception())}")
|
|
return
|
|
return super().handle()
|
|
|
|
|
|
def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult):
|
|
"""
|
|
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()
|
|
|
|
|
|
def setup_server():
|
|
"""
|
|
Helps setup a http/https server
|
|
"""
|
|
try:
|
|
gMe.op.server = http.server.ThreadingHTTPServer(gMe.nw.server_address(), ProxyHandler)
|
|
if gMe.sec.get('keyFile') and gMe.sec.get('certFile'):
|
|
sslCtxt = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
sslCtxt.load_cert_chain(certfile=gMe.sec.certFile, keyfile=gMe.sec.keyFile)
|
|
sslCtxt.minimum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED
|
|
sslCtxt.maximum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED
|
|
gMe.op.sslContext = sslCtxt
|
|
print(f"INFO:SetupServer:Starting on {gMe.nw.server_address()}:Https mode")
|
|
else:
|
|
print(f"INFO:SetupServer:Starting on {gMe.nw.server_address()}:Http mode")
|
|
except Exception as exc:
|
|
print(f"ERRR:SetupServer:{traceback.format_exc()}")
|
|
raise RuntimeError(f"SetupServer:{exc}") from exc
|
|
|
|
|
|
def run():
|
|
try:
|
|
setup_server()
|
|
if not gMe.op.server:
|
|
raise RuntimeError("Server missing!!!")
|
|
gMe.op.server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
print("INFO:Run:Shuting down...")
|
|
if gMe.op.server:
|
|
gMe.op.server.server_close()
|
|
sys.exit(0)
|
|
except Exception as exc:
|
|
print(f"ERRR:Run:Exiting:Exception:{exc}")
|
|
if gMe.op.server:
|
|
gMe.op.server.server_close()
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
gMe.process_args(sys.argv)
|
|
run()
|