146 lines
7.1 KiB
Python
146 lines
7.1 KiB
Python
"""Actual HTTP MCP startup is independent of ambient WebSocket libraries."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
from framebridge_expanded import __version__
|
|
|
|
SOURCE = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class HttpMcpTransportTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temporary = tempfile.TemporaryDirectory(prefix='fb-http-transport-')
|
|
self.addCleanup(self.temporary.cleanup)
|
|
self.directory = Path(self.temporary.name)
|
|
self.ambient = self.directory/'ambient'
|
|
package = self.ambient/'websockets'
|
|
package.mkdir(parents=True)
|
|
self.marker = self.directory/'unwanted-websocket-import'
|
|
(package/'__init__.py').write_text(
|
|
"import os\nfrom pathlib import Path\n"
|
|
"Path(os.environ['FB_TEST_WS_MARKER']).write_text('imported')\n"
|
|
"raise RuntimeError('INCOMPATIBLE_AMBIENT_WEBSOCKETS')\n")
|
|
home = self.directory/'home'
|
|
home.mkdir(mode=0o700)
|
|
self.env = dict(os.environ, HOME=str(home), PYTHONNOUSERSITE='1',
|
|
PYTHONDONTWRITEBYTECODE='1',
|
|
FB_TEST_WS_MARKER=str(self.marker),
|
|
FRAMEBRIDGE_EXPANDED_STATE=str(home/'state'),
|
|
FRAMEBRIDGE_STATE_DIR=str(home/'state'),
|
|
FRAMEBRIDGE_TOOL_ROOT=str(home/'adapter'),
|
|
FRAMEBRIDGE_BROKER_SOURCE=str(SOURCE/'sources/original-geranas/FrameBridge-Broker-RC1/src'),
|
|
FRAMEBRIDGE_FLEET_SOURCE=str(SOURCE/'sources/original-geranas/FrameBridge-Fleet-Hub/app'))
|
|
# Match the child search path exported by runtime.environment(), with an
|
|
# intentionally incompatible optional library taking precedence.
|
|
self.env['PYTHONPATH'] = os.pathsep.join(map(str, [
|
|
self.ambient, SOURCE/'expanded', SOURCE/'portable',
|
|
SOURCE/'sources/original-geranas/FrameBridge-Fleet-Hub/vendor']))
|
|
|
|
def test_default_websocket_detection_would_load_the_incompatible_library(self):
|
|
code = "from mcp.server.fastmcp import FastMCP; import uvicorn; uvicorn.Config(FastMCP('negative-control').streamable_http_app()).load()"
|
|
result = subprocess.run([sys.executable, '-c', code], env=self.env,
|
|
capture_output=True, text=True, timeout=15)
|
|
self.assertNotEqual(0, result.returncode)
|
|
self.assertIn('INCOMPATIBLE_AMBIENT_WEBSOCKETS', result.stderr)
|
|
self.assertTrue(self.marker.exists())
|
|
|
|
def start(self, name):
|
|
with socket.socket() as reservation:
|
|
reservation.bind(('127.0.0.1', 0))
|
|
self.port = reservation.getsockname()[1]
|
|
if name == 'fleet':
|
|
command = [sys.executable, '-m', 'framebridge_expanded.fleet']
|
|
self.env['FRAMEBRIDGE_FLEET_HUB_PORT'] = str(self.port)
|
|
else:
|
|
command = [sys.executable, str(SOURCE/'portable/front_door_mcp.py')]
|
|
self.env['FRAMEBRIDGE_FRONT_DOOR_PORT'] = str(self.port)
|
|
log = (self.directory/(name+'.log')).open('w')
|
|
self.process = subprocess.Popen(command, env=self.env, cwd=self.directory,
|
|
stdin=subprocess.DEVNULL, stdout=log,
|
|
stderr=log, start_new_session=True)
|
|
log.close()
|
|
self.addCleanup(self.stop)
|
|
self.session = None
|
|
self.protocol = None
|
|
deadline = time.monotonic()+12
|
|
while time.monotonic() < deadline:
|
|
if self.process.poll() is not None:
|
|
self.fail('HTTP component exited: '+(self.directory/(name+'.log')).read_text()[-2000:])
|
|
try:
|
|
reply, headers, _ = self.rpc({'jsonrpc':'2.0', 'id':1,
|
|
'method':'initialize', 'params':{'protocolVersion':'2025-11-25',
|
|
'capabilities':{}, 'clientInfo':{'name':'framebridge-http-qa','version':'1'}}})
|
|
break
|
|
except urllib.error.URLError:
|
|
time.sleep(.05)
|
|
else:
|
|
self.fail('HTTP component did not become ready')
|
|
self.assertIn('result', reply)
|
|
self.assertIn('serverInfo', reply['result'])
|
|
self.assertTrue(headers.get('Content-Type', '').startswith('text/event-stream'))
|
|
self.session = headers.get('Mcp-Session-Id')
|
|
self.assertTrue(self.session)
|
|
self.protocol = reply['result']['protocolVersion']
|
|
_, _, code = self.rpc({'jsonrpc':'2.0', 'method':'notifications/initialized'})
|
|
self.assertEqual(202, code)
|
|
|
|
def stop(self):
|
|
if self.process.poll() is None:
|
|
self.process.terminate()
|
|
try:
|
|
self.process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
self.process.kill()
|
|
self.process.wait(timeout=5)
|
|
|
|
def rpc(self, value):
|
|
headers = {'Content-Type':'application/json',
|
|
'Accept':'application/json, text/event-stream'}
|
|
if self.session:
|
|
headers['Mcp-Session-Id'] = self.session
|
|
if self.protocol:
|
|
headers['Mcp-Protocol-Version'] = self.protocol
|
|
request = urllib.request.Request('http://127.0.0.1:'+str(self.port)+'/mcp',
|
|
data=json.dumps(value).encode(), headers=headers, method='POST')
|
|
with urllib.request.urlopen(request, timeout=3) as response:
|
|
raw = response.read().decode()
|
|
if raw and response.headers.get('Content-Type','').startswith('text/event-stream'):
|
|
raw = next(line[5:].strip() for line in raw.splitlines() if line.startswith('data:'))
|
|
return json.loads(raw) if raw else None, response.headers, response.status
|
|
|
|
def test_fleet_initializes_and_lists_tools_without_websocket_import(self):
|
|
self.start('fleet')
|
|
result, _, status = self.rpc({'jsonrpc':'2.0','id':2,'method':'tools/list'})
|
|
self.assertEqual(200, status)
|
|
self.assertEqual(6, len(result['result']['tools']))
|
|
self.assertFalse(self.marker.exists())
|
|
|
|
def test_actual_frontdoor_help_and_execution_denial_over_streamable_http(self):
|
|
self.start('frontdoor')
|
|
result, _, status = self.rpc({'jsonrpc':'2.0','id':2,'method':'tools/list'})
|
|
self.assertEqual(200, status)
|
|
names = {tool['name'] for tool in result['result']['tools']}
|
|
self.assertTrue({'framebridge_help','framebridge_exec'}.issubset(names))
|
|
reply, _, _ = self.rpc({'jsonrpc':'2.0','id':3,'method':'tools/call',
|
|
'params':{'name':'framebridge_help','arguments':{'topic':'start'}}})
|
|
self.assertIn(__version__, json.dumps(reply['result']))
|
|
denied, _, _ = self.rpc({'jsonrpc':'2.0','id':4,'method':'tools/call',
|
|
'params':{'name':'framebridge_exec',
|
|
'arguments':{'script':'true','authorized':False}}})
|
|
self.assertIn('AUTHORIZED_TRUE_REQUIRED', json.dumps(denied['result']))
|
|
self.assertFalse((Path(self.env['FRAMEBRIDGE_TOOL_ROOT'])/'adapter.sock').exists())
|
|
self.assertFalse(self.marker.exists())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|