Skip to content

Commit 73bbb92

Browse files
committed
mcp reverse proxy
Signed-off-by: John <johnandersenpdx@gmail.com>
1 parent 57e800a commit 73bbb92

File tree

6 files changed

+341
-10
lines changed

6 files changed

+341
-10
lines changed

github_webhook_events/Caddyfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
# Expose Caddy’s admin API on a Unix socket
3+
admin unix/{{CALLER_PATH}}/caddy-admin.sock
4+
auto_https off
5+
log default {
6+
level debug
7+
}
8+
}
9+
10+
# Listen on a Unix socket
11+
http://127.0.0.1 {
12+
bind unix/{{CLIENT_SIDE_MCP_REVERSE_PROXY_SOCKET_PATH}}
13+
14+
reverse_proxy unix/{{CALLER_PATH}}/caddy-admin.sock
15+
}

github_webhook_events/agi.py

Lines changed: 185 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
AGI_SOCK=/tmp/agi.sock go run agi_sshd.go
88
9-
export INPUT_SOCK="$(mktemp -d)/input.sock"; export OUTPUT_SOCK="$(mktemp -d)/text-output.sock"; export NDJSON_OUTPUT_SOCK="$(mktemp -d)/ndjson-output.sock"; ssh -NnT -p 2222 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o PasswordAuthentication=no -R /tmux.sock:$(echo $TMUX | sed -e 's/,.*//g') -R "${OUTPUT_SOCK}:${OUTPUT_SOCK}" -R "${NDJSON_OUTPUT_SOCK}:${NDJSON_OUTPUT_SOCK}" -R "${INPUT_SOCK}:${INPUT_SOCK}" user@localhost
9+
export INPUT_SOCK="$(mktemp -d)/input.sock"; export OUTPUT_SOCK="$(mktemp -d)/text-output.sock"; export NDJSON_OUTPUT_SOCK="$(mktemp -d)/ndjson-output.sock"; export MCP_REVERSE_PROXY_SOCK="$(mktemp -d)/mcp-reverse-proxy.sock"; ssh -NnT -p 2222 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o PasswordAuthentication=no -R /tmux.sock:$(echo $TMUX | sed -e 's/,.*//g') -R "${OUTPUT_SOCK}:${OUTPUT_SOCK}" -R "${NDJSON_OUTPUT_SOCK}:${NDJSON_OUTPUT_SOCK}" -R "${MCP_REVERSE_PROXY_SOCK}:${MCP_REVERSE_PROXY_SOCK}" -R "${INPUT_SOCK}:${INPUT_SOCK}" user@localhost
1010
1111
1212
gh auth refresh -h github.com -s admin:public_key
@@ -3323,6 +3323,18 @@ def make_argparse_parser(argv=None):
33233323
default=None,
33243324
type=str,
33253325
)
3326+
parser.add_argument(
3327+
"--mcp-reverse-proxy-socket-path",
3328+
dest="mcp_reverse_proxy_socket_path",
3329+
default=None,
3330+
type=str,
3331+
)
3332+
parser.add_argument(
3333+
"--client-side-mcp-reverse-proxy-socket-path",
3334+
dest="client_side_mcp_reverse_proxy_socket_path",
3335+
default=None,
3336+
type=str,
3337+
)
33263338
parser.add_argument(
33273339
"--agi-name",
33283340
dest="agi_name",
@@ -3650,6 +3662,68 @@ class AGIThreadNotFoundError(Exception):
36503662
pass
36513663

36523664

3665+
import http
3666+
import httpx
3667+
3668+
3669+
class CaddyConfigLoadError(Exception):
3670+
pass
3671+
3672+
3673+
async def caddy_config_update(mcp_reverse_proxy_socket_path, slug):
3674+
transport = httpx.AsyncHTTPTransport(uds=mcp_reverse_proxy_socket_path)
3675+
async with httpx.AsyncClient(transport=transport) as client:
3676+
# TODO Booooooooo timeout booooooo
3677+
time = 0
3678+
success = False
3679+
while not success and time < 5:
3680+
try:
3681+
await client.get("http://127.0.0.1/config/")
3682+
success = True
3683+
except httpx.RemoteProtocolError:
3684+
await asyncio.sleep(0.1)
3685+
time += 0.1
3686+
response = await client.get(
3687+
"http://127.0.0.1/config/",
3688+
)
3689+
caddy_config = response.json()
3690+
exists = False
3691+
proxy_route_caddy_admin = None
3692+
for route in caddy_config['apps']['http']['servers']['srv0']['routes']:
3693+
for match in route['match']:
3694+
for host in match['host']:
3695+
if host == '127.0.0.1':
3696+
proxy_route_caddy_admin = route
3697+
elif host == slug:
3698+
exists = True
3699+
if exists:
3700+
return
3701+
proxy_route_new = json.loads(
3702+
json.dumps(
3703+
proxy_route_caddy_admin,
3704+
).replace(
3705+
"caddy-admin", slug,
3706+
).replace(
3707+
"127.0.0.1", slug,
3708+
),
3709+
)
3710+
caddy_config['apps']['http']['servers']['srv0']['routes'].append(
3711+
proxy_route_new,
3712+
)
3713+
response = await client.post(
3714+
"http://127.0.0.1/load",
3715+
headers={"Content-Type": "application/json"},
3716+
content=json.dumps(caddy_config),
3717+
)
3718+
if response.status_code != http.HTTPStatus.OK.value:
3719+
raise CaddyConfigLoadError(f"{response.status_code}: {response.text}")
3720+
response = await client.get(
3721+
"http://127.0.0.1/config/",
3722+
)
3723+
caddy_config = response.json()
3724+
snoop.pp(caddy_config)
3725+
3726+
36533727
async def agent_openai(
36543728
tg: asyncio.TaskGroup,
36553729
async_exit_stack: contextlib.AsyncExitStack,
@@ -3660,6 +3734,7 @@ async def agent_openai(
36603734
action_stream_insert: Callable[[Any], Awaitable[Any]],
36613735
waiting_event_stream_insert: Callable[[Any], Awaitable[Any]],
36623736
openai_api_key: str,
3737+
mcp_reverse_proxy_socket_path: str,
36633738
*,
36643739
openai_base_url: Optional[str] = None,
36653740
):
@@ -3678,12 +3753,38 @@ async def agent_openai(
36783753
# params={"command": "uvx", "args": ["mcp-server-git"]},
36793754
# )
36803755
# )
3681-
# mcp_server_workflow = await async_exit_stack.enter_async_context(
3682-
# openai_agents_mcp.MCPServerStdio(
3683-
# # cache_tools_list=True, # Cache the tools list, for demonstration
3684-
# params={"command": "python", "args": ["-c", "import mcp from agi; mcp.run(transport='stdio')"]},
3685-
# )
3686-
# )
3756+
# TODO Actions for adding more MCP servers, for N-1 in stack actions for
3757+
# starting them on the client, for N-1 in stack writing new ones and
3758+
# analysis and threat model trust boundry stuff.
3759+
# TODO These should be a class that's part of the action data
3760+
mcp_servers = [
3761+
{
3762+
"name": "File Resource Server",
3763+
"slug": "files",
3764+
},
3765+
# {
3766+
# "name": "/usr/bin/ss network utility",
3767+
# "slug": "bin-ss",
3768+
# },
3769+
]
3770+
mcp_servers_workflow = []
3771+
3772+
transport = httpx.AsyncHTTPTransport(uds=mcp_reverse_proxy_socket_path)
3773+
for mcp_server in mcp_servers:
3774+
await caddy_config_update(mcp_reverse_proxy_socket_path, mcp_server['slug'])
3775+
mcp_servers_workflow.append(
3776+
await async_exit_stack.enter_async_context(
3777+
openai_agents_mcp.MCPServerSse(
3778+
name=mcp_server["name"],
3779+
params={
3780+
"url": f"http://{mcp_server['slug']}/sse",
3781+
"transport": transport,
3782+
},
3783+
),
3784+
),
3785+
)
3786+
3787+
snoop.pp("MCP servers have been setup")
36873788

36883789
agents = {}
36893790
threads = {}
@@ -3744,11 +3845,19 @@ async def agent_openai(
37443845
37453846
shell: interpreter {0}
37463847
3848+
By default you should use `shell: bash -xe {0}`.
3849+
37473850
Assume {0} is a temporary file containing the
37483851
contents the code you place in `run`.
3852+
3853+
You should not include use of actions/checkout
3854+
unless specifically requested to checkout the current
3855+
repo.
37493856
""".strip(),
37503857
),
3751-
# mcp_servers=[mcp_server_workflow],
3858+
# TODO Dynamically auto discover applicable MCPs and add
3859+
# them to agents
3860+
# mcp_servers=mcp_servers_workflow,
37523861
output_type=PolicyEngineWorkflow,
37533862
)
37543863

@@ -4311,9 +4420,11 @@ async def main(
43114420
input_socket_path: Optional[str] = None,
43124421
text_output_socket_path: Optional[str] = None,
43134422
ndjson_output_socket_path: Optional[str] = None,
4423+
mcp_reverse_proxy_socket_path: Optional[str] = None,
43144424
client_side_input_socket_path: Optional[str] = None,
43154425
client_side_text_output_socket_path: Optional[str] = None,
43164426
client_side_ndjson_output_socket_path: Optional[str] = None,
4427+
client_side_mcp_reverse_proxy_socket_path: Optional[str] = None,
43174428
):
43184429
if log is not None:
43194430
# logging.basicConfig(level=log)
@@ -4351,7 +4462,16 @@ async def main(
43514462
write_ndjson_output = write_unix_socket(ndjson_output_socket_path)
43524463
await write_ndjson_output.asend(None)
43534464

4465+
async def error_handler_send_error_to_client(exc_type, exc_value, traceback):
4466+
nonlocal write_ndjson_output
4467+
output_message = OutputMessage(
4468+
work_name=f"main.error.halted",
4469+
result=f"{exc_type} {exc_value} {traceback}",
4470+
)
4471+
await write_ndjson_output.asend(f"{output_message.model_dump_json()}\n".encode())
4472+
43544473
async with kvstore, asyncio.TaskGroup() as tg, contextlib.AsyncExitStack() as async_exit_stack:
4474+
async_exit_stack.push_async_exit(error_handler_send_error_to_client)
43554475
# Raw Input Action Stream
43564476
unvalidated_user_input_action_stream = pdb_action_stream(
43574477
tg,
@@ -4401,6 +4521,7 @@ async def user_input_action_stream_queue_iterator(queue):
44014521
action_stream_insert,
44024522
waiting_event_stream_insert,
44034523
openai_api_key,
4524+
mcp_reverse_proxy_socket_path,
44044525
openai_base_url=openai_base_url,
44054526
)
44064527
else:
@@ -4777,9 +4898,11 @@ async def tmux_test(
47774898
input_socket_path: Optional[str] = None,
47784899
text_output_socket_path: Optional[str] = None,
47794900
ndjson_output_socket_path: Optional[str] = None,
4901+
mcp_reverse_proxy_socket_path: Optional[str] = None,
47804902
client_side_input_socket_path: Optional[str] = None,
47814903
client_side_text_output_socket_path: Optional[str] = None,
47824904
client_side_ndjson_output_socket_path: Optional[str] = None,
4905+
client_side_mcp_reverse_proxy_socket_path: Optional[str] = None,
47834906
**kwargs
47844907
):
47854908
pane = None
@@ -5046,6 +5169,49 @@ async def tmux_test(
50465169
pane.send_keys(f'socat UNIX-LISTEN:${agi_name.upper()}_INPUT_SOCK,fork EXEC:"/usr/bin/tail -F ${agi_name.upper()}_INPUT" &', enter=True)
50475170
pane.send_keys(f'ls -lAF ${agi_name.upper()}_INPUT', enter=True)
50485171

5172+
pane.send_keys(
5173+
'cat > "${CALLER_PATH}/mcp_server_files.py" <<\'WRITE_OUT_SH_EOF\''
5174+
+ "\n"
5175+
+ pathlib.Path(__file__).parent.joinpath("mcp_server_files.py").read_text(),
5176+
enter=True,
5177+
)
5178+
pane.send_keys('', enter=True)
5179+
pane.send_keys('WRITE_OUT_SH_EOF', enter=True)
5180+
5181+
pane.send_keys(
5182+
textwrap.dedent(
5183+
'''
5184+
if [ ! -f "${CALLER_PATH}/mcp_server_files.logs.txt" ]; then
5185+
python -u ${CALLER_PATH}/mcp_server_files.py --transport sse --uds ${CALLER_PATH}/files.sock 1>"${CALLER_PATH}/mcp_server_files.logs.txt" 2>&1 &
5186+
tail -F "${CALLER_PATH}/mcp_server_files.logs.txt" &
5187+
MCP_SERVER_FILES_PID=$!
5188+
fi
5189+
'''.lstrip(),
5190+
),
5191+
enter=True,
5192+
)
5193+
5194+
pane.send_keys(
5195+
'cat > "${CALLER_PATH}/Caddyfile" <<\'WRITE_OUT_SH_EOF\''
5196+
+ "\n"
5197+
+ pathlib.Path(__file__).parent.joinpath("Caddyfile").read_text().replace("{{CALLER_PATH}}", tempdir).replace("{{CLIENT_SIDE_MCP_REVERSE_PROXY_SOCKET_PATH}}", client_side_mcp_reverse_proxy_socket_path),
5198+
enter=True,
5199+
)
5200+
pane.send_keys('', enter=True)
5201+
pane.send_keys('WRITE_OUT_SH_EOF', enter=True)
5202+
5203+
# if [ ! -f "${CALLER_PATH}/caddy.logs.txt" ]; then
5204+
pane.send_keys(
5205+
textwrap.dedent(
5206+
'''
5207+
HOME=${CALLER_PATH} caddy run --config ${CALLER_PATH}/Caddyfile 1>"${CALLER_PATH}/caddy.logs.txt" 2>&1 &
5208+
tail -F "${CALLER_PATH}/caddy.logs.txt" &
5209+
CADDY_PID=$!
5210+
'''.lstrip(),
5211+
),
5212+
enter=True,
5213+
)
5214+
50495215
pane.send_keys(f'set +x', enter=True)
50505216

50515217
await main(
@@ -5055,8 +5221,10 @@ async def tmux_test(
50555221
client_side_input_socket_path=client_side_input_socket_path,
50565222
text_output_socket_path=text_output_socket_path,
50575223
ndjson_output_socket_path=ndjson_output_socket_path,
5224+
mcp_reverse_proxy_socket_path=mcp_reverse_proxy_socket_path,
50585225
client_side_text_output_socket_path=client_side_text_output_socket_path,
50595226
client_side_ndjson_output_socket_path=client_side_ndjson_output_socket_path,
5227+
client_side_mcp_reverse_proxy_socket_path=client_side_mcp_reverse_proxy_socket_path,
50605228
**kwargs
50615229
)
50625230
finally:
@@ -5091,7 +5259,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
50915259
)
50925260

50935261

5094-
def run_tmux_attach(socket_path, input_socket_path, client_side_input_socket_path, text_output_socket_path, client_side_text_output_socket_path, ndjson_output_socket_path, client_side_ndjson_output_socket_path):
5262+
def run_tmux_attach(socket_path, input_socket_path, client_side_input_socket_path, text_output_socket_path, client_side_text_output_socket_path, ndjson_output_socket_path, client_side_ndjson_output_socket_path, mcp_reverse_proxy_socket_path, client_side_mcp_reverse_proxy_socket_path):
50955263
cmd = [
50965264
sys.executable,
50975265
"-u",
@@ -5110,6 +5278,10 @@ def run_tmux_attach(socket_path, input_socket_path, client_side_input_socket_pat
51105278
ndjson_output_socket_path,
51115279
"--client-side-ndjson-output-socket-path",
51125280
client_side_ndjson_output_socket_path,
5281+
"--mcp-reverse-proxy-socket-path",
5282+
mcp_reverse_proxy_socket_path,
5283+
"--client-side-mcp-reverse-proxy-socket-path",
5284+
client_side_mcp_reverse_proxy_socket_path,
51135285
"--agi-name",
51145286
# TODO Something secure here, scitt URN and lookup for PS1?
51155287
f"alice{str(uuid.uuid4()).split('-')[4]}",
@@ -5142,9 +5314,11 @@ class RequestConnectTMUX(BaseModel):
51425314
socket_input_path: str = Field(alias="input.sock")
51435315
socket_text_output_path: str = Field(alias="text-output.sock")
51445316
socket_ndjson_output_path: str = Field(alias="ndjson-output.sock")
5317+
socket_mcp_reverse_proxy_path: str = Field(alias="mcp-reverse-proxy.sock")
51455318
socket_client_side_input_path: str = Field(alias="client-side-input.sock")
51465319
socket_client_side_text_output_path: str = Field(alias="client-side-text-output.sock")
51475320
socket_client_side_ndjson_output_path: str = Field(alias="client-side-ndjson-output.sock")
5321+
socket_client_side_mcp_reverse_proxy_path: str = Field(alias="client-side-mcp-reverse-proxy.sock")
51485322

51495323

51505324
@app.post("/connect/tmux")
@@ -5158,6 +5332,8 @@ async def connect(request_connect_tmux: RequestConnectTMUX, background_tasks: Ba
51585332
request_connect_tmux.socket_client_side_text_output_path,
51595333
request_connect_tmux.socket_ndjson_output_path,
51605334
request_connect_tmux.socket_client_side_ndjson_output_path,
5335+
request_connect_tmux.socket_mcp_reverse_proxy_path,
5336+
request_connect_tmux.socket_client_side_mcp_reverse_proxy_path,
51615337
)
51625338
return {
51635339
"connected": True,

github_webhook_events/agi_sshd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func handleSSH(raw net.Conn, cfg *ssh.ServerConfig) {
135135
req.Reply(true, nil)
136136
go acceptLoop(ctx, listener, serverConn, p.SocketPath)
137137

138-
if !notified && count >= 4 {
138+
if !notified && count >= 5 {
139139
notified = true
140140
go notifyAGI(ctx, &mu, forwards)
141141
}
@@ -221,6 +221,9 @@ func notifyAGI(ctx context.Context, mu *sync.Mutex, forwards map[string]*forward
221221
if strings.HasSuffix(f.rawPath, "ndjson-output.sock") {
222222
data["client-side-ndjson-output.sock"] = f.rawPath
223223
}
224+
if strings.HasSuffix(f.rawPath, "mcp-reverse-proxy.sock") {
225+
data["client-side-mcp-reverse-proxy.sock"] = f.rawPath
226+
}
224227
}
225228
mu.Unlock()
226229

0 commit comments

Comments
 (0)