Skip to content

Commit 7f39a83

Browse files
authored
Merge pull request #2 from GitHubSecurityLab/codeql_streamable
Local MCP streamable server support
2 parents 664d2a3 + 27b2508 commit 7f39a83

File tree

10 files changed

+355
-120
lines changed

10 files changed

+355
-120
lines changed

README.md

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,43 @@ It's primary value proposition is as a CLI tool that allows users to quickly def
1414

1515
Agents are defined through [personalities](personalities/), that receive a [task](taskflows/) to complete given a set of [tools](toolboxes/).
1616

17-
Agents can cooperate to complete sequences of tasks through so-called [Taskflows](taskflows/GRAMMAR.md).
17+
Agents can cooperate to complete sequences of tasks through so-called [taskflows](taskflows/GRAMMAR.md).
18+
19+
You can find a detailed overview of the taskflow grammar [here](https://github.com/GitHubSecurityLab/seclab-taskflow-agent/blob/main/taskflows/GRAMMAR.md) and example taskflows [here](https://github.com/GitHubSecurityLab/seclab-taskflow-agent/tree/main/taskflows/examples).
20+
21+
## Use Cases and Examples
22+
23+
The Seclab Taskflow Agent framework was primarily designed to fit the iterative feedback loop driven work involved in Agentic security research workflows and vulnerability triage tasks.
24+
25+
Its design philosophy is centered around the belief that a prompt level focus of capturing vulnerability patterns will greatly improve and scale security research results as frontier model capabilities evolve over time.
26+
27+
While the maintainer himself primarily uses this framework as a code auditing tool it also serves as a more generic swiss army knife for exploring Agentic workflows. For example, the GitHub Security Lab also uses this framework for automated code scanning alert triage.
28+
29+
The framework includes a [CodeQL](https://codeql.github.com/) MCP server that can be used for Agentic code review, see the [CVE-2023-2283](https://github.com/GitHubSecurityLab/seclab-taskflow-agent/blob/main/taskflows/CVE-2023-2283/CVE-2023-2283.yaml) for an example of how to have an Agent review C code using a CodeQL database.
30+
31+
Instead of generating CodeQL queries itself, the CodeQL MCP Server is used to provide CodeQL-query based MCP tools that allow an Agent to navigate and explore code. It leverages templated CodeQL queries to provide targeted context for model driven code analysis.
1832

1933
## Requirements
2034

2135
Python >= 3.9 or Docker
2236

23-
# Usage
37+
## Configuration
38+
39+
Provide a GitHub token for an account that is entitled to use GitHub Copilot via the `COPILOT_TOKEN` environment variable. Further configuration is use case dependent, i.e. pending which MCP servers you'd like to use in your taskflows.
40+
41+
You can set persisting environment variables via an `.env` file in the project root.
2442

25-
Provide a Copilot entitled GitHub PAT via the `COPILOT_TOKEN` environment variable.
43+
Example:
2644

27-
## Source
45+
```sh
46+
# Tokens
47+
COPILOT_TOKEN=<your_github_token>
48+
# MCP configs
49+
GITHUB_PERSONAL_ACCESS_TOKEN=<your_github_token>
50+
CODEQL_DBS_BASE_PATH="/app/my_data/"
51+
```
52+
53+
## Deploying from Source
2854

2955
First install the required dependencies:
3056

@@ -48,40 +74,42 @@ Example: deploying a Taskflow:
4874
python main.py -t example
4975
```
5076

51-
## Docker
77+
## Deploying from Docker
5278

53-
Alternatively you can deploy the Agent via its Docker image using `docker/run.sh`.
79+
You can deploy the Taskflow Agent via its Docker image using `docker/run.sh`.
80+
81+
WARNING: the Agent Docker image is _NOT_ intended as a security boundary but strictly a deployment convenience.
5482

5583
The image entrypoint is `main.py` and thus it operates the same as invoking the Agent from source directly.
5684

5785
You can find the Docker image for the Seclab Taskflow Agent [here](https://github.com/GitHubSecurityLab/seclab-taskflow-agent/pkgs/container/seclab-taskflow-agent) and how it is built [here](release_tools/).
5886

5987
Note that this image is based on a public release of the Taskflow Agent, and you will have to mount any custom taskflows, personalities, or prompts into the image for them to be available to the Agent.
6088

61-
See [docker/run.sh](docker/run.sh) for configuration details.
89+
Optional image mount points to supply custom data are configured via the environment:
6290

63-
Example: deploying a Taskflow:
91+
- Custom data via `MY_DATA`, mounts to `/app/my_data`
92+
- Custom personalities via `MY_PERSONALITIES`, mounts to `/app/personalities/my_personalities`
93+
- Custom taskflows via `MY_TASKFLOWS`, mounts to `/app/taskflows/my_taskflows`
94+
- Custom prompts via `MY_PROMPTS`, mounts to `/app/prompts/my_prompts`
95+
- Custom toolboxes via `MY_TOOLBOXES`, mounts to `/app/toolboxes/my_toolboxes`
96+
97+
See [docker/run.sh](docker/run.sh) for further details.
98+
99+
Example: deploying a Taskflow (example.yaml):
64100

65101
```sh
66102
docker/run.sh -t example
67103
```
68-
Example: deploying a custom taskflow:
104+
Example: deploying a custom taskflow (custom_taskflow.yaml):
69105

70106
```sh
71107
MY_TASKFLOWS=~/my_taskflows docker/run.sh -t custom_taskflow
72108
```
73109

74-
Available image mount points are:
75-
76-
- Custom data via `MY_DATA` environment variable
77-
- Custom personalities via `MY_PERSONALITIES` environment variable
78-
- Custom taskflows via `MY_TASKFLOWS` environment variable
79-
- Custom prompts via `MY_PROMPTS` environment variable
80-
- Custom toolboxes via `MY_TOOLBOXES` environment variable
81-
82110
For more advanced scenarios like e.g. making custom MCP server code available, you can alter the run script to mount your custom code into the image and configure your toolboxes to use said code accordingly.
83111

84-
Example: custom MCP server deployment via Docker image:
112+
Example: a custom MCP server deployment via Docker image:
85113

86114
```sh
87115
export MY_MCP_SERVERS=./mcp_servers
@@ -109,7 +137,7 @@ docker run \
109137

110138
Our default run script makes the Docker socket available to the image, which contains the Docker cli, so 3rd party Docker based stdio MCP servers also function as normal.
111139

112-
Example: a toolbox configuration for the official GitHub MCP Server:
140+
Example: a toolbox configuration using the official GitHub MCP Server via Docker:
113141

114142
```yaml
115143
server_params:
@@ -120,23 +148,7 @@ server_params:
120148
GITHUB_PERSONAL_ACCESS_TOKEN: "{{ env GITHUB_PERSONAL_ACCESS_TOKEN }}"
121149
```
122150
123-
## Framework Configuration
124-
125-
Set environment variables via an `.env` file in the project root.
126-
127-
Example: a persistent Agent configuration with various MCP server environment variables set:
128-
129-
```sh
130-
# Tokens
131-
COPILOT_TOKEN=...
132-
# Docker config, MY_DATA is mounted to /app/my_data
133-
MY_DATA="/home/user/my_data"
134-
# MCP configs
135-
GITHUB_PERSONAL_ACCESS_TOKEN=...
136-
CODEQL_DBS_BASE_PATH="/app/my_data/"
137-
```
138-
139-
# Personalities
151+
## Personalities
140152
141153
Core characteristics for a single Agent. Configured through YAML files in `personalities/`.
142154

@@ -157,7 +169,7 @@ toolboxes:
157169
- echo
158170
```
159171

160-
# Toolboxes
172+
## Toolboxes
161173

162174
MCP servers that provide tools. Configured through YAML files in `toolboxes/`.
163175

@@ -174,18 +186,7 @@ server_params:
174186
SOME: value
175187
```
176188

177-
Example sse config:
178-
179-
```yaml
180-
server_params:
181-
kind: sse
182-
# make sure you .env config the echo server, see echo_sse.py for example
183-
url: http://127.0.0.1:9000/echo
184-
headers:
185-
SomeHeader: "{{ env USER }}"
186-
```
187-
188-
# Taskflows
189+
## Taskflows
189190

190191
A sequence of interdependent tasks performed by a set of Agents. Configured through a YAML based [grammar](taskflows/GRAMMAR.md) in [taskflows/](taskflows/).
191192

@@ -263,6 +264,6 @@ This project is licensed under the terms of the MIT open source license. Please
263264

264265
[SUPPORT](./SUPPORT.md)
265266

266-
## Acknowledgement
267+
## Acknowledgements
267268

268-
Security Lab team members @m-y-mo and @p- for contributing heavily to the testing and development of this framework, as well as the rest of the Security Lab team for helpful discussions and use cases.
269+
Security Lab team members [Man Yue Mo](https://github.com/m-y-mo) and [Peter Stockli](https://github.com/p-) for contributing heavily to the testing and development of this framework, as well as the rest of the Security Lab team for helpful discussions and feedback.

main.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,12 @@
2323
from typing import Any
2424

2525
from shell_utils import shell_tool_call
26-
from mcp_utils import DEFAULT_MCP_CLIENT_SESSION_TIMEOUT, ReconnectingMCPServerStdio, AsyncDebugMCPServerStdio, MCPNamespaceWrap
26+
from mcp_utils import DEFAULT_MCP_CLIENT_SESSION_TIMEOUT, ReconnectingMCPServerStdio, AsyncDebugMCPServerStdio, MCPNamespaceWrap, mcp_client_params, mcp_system_prompt, StreamableMCPThread
2727
from render_utils import render_model_output, flush_async_output
2828
from env_utils import TmpEnv
2929
from yaml_parser import YamlParser
3030
from agent import TaskAgent
3131
from capi import list_tool_call_models
32-
from mcp_utils import mcp_client_params
33-
from mcp_utils import mcp_system_prompt
3432

3533
load_dotenv()
3634

@@ -132,27 +130,47 @@ async def deploy_task_agents(agents: dict,
132130
# XXX: auto-allow all tools if task is headless by clearing confirms
133131
confirms = []
134132
client_session_timeout = client_session_timeout or DEFAULT_MCP_CLIENT_SESSION_TIMEOUT
133+
server_proc = None
135134
match params['kind']:
135+
# since we spawn stdio servers each time we do not expect
136+
# new tools to appear over time so cache the tools list
136137
case 'stdio':
137138
if params.get('reconnecting', False):
138139
mcp_server = ReconnectingMCPServerStdio(
139140
name=tb,
140141
params=params,
141142
tool_filter=tool_filter,
142-
client_session_timeout_seconds=client_session_timeout)
143+
client_session_timeout_seconds=client_session_timeout,
144+
cache_tools_list=True)
143145
else:
144146
mcp_server = MCPServerStdio(
145147
name=tb,
146148
params=params,
147149
tool_filter=tool_filter,
148-
client_session_timeout_seconds=client_session_timeout)
150+
client_session_timeout_seconds=client_session_timeout,
151+
cache_tools_list=True)
149152
case 'sse':
150153
mcp_server = MCPServerSse(
151154
name=tb,
152155
params=params,
153156
tool_filter=tool_filter,
154157
client_session_timeout_seconds=client_session_timeout)
155-
case 'streamable': # XXX: needs testing
158+
case 'streamable':
159+
# check if we need to start this server locally as well
160+
if 'command' in params:
161+
def _print_out(line):
162+
msg = f"Streamable MCP Server stdout: {line}"
163+
logging.info(msg)
164+
#print(msg)
165+
def _print_err(line):
166+
msg = f"Streamable MCP Server stderr: {line}"
167+
logging.info(msg)
168+
#print(msg)
169+
server_proc = StreamableMCPThread(params['command'],
170+
url=params['url'],
171+
env=params['env'],
172+
on_output=_print_out,
173+
on_error=_print_err)
156174
mcp_server = MCPServerStreamableHttp(
157175
name=tb,
158176
params=params,
@@ -161,7 +179,7 @@ async def deploy_task_agents(agents: dict,
161179
case _:
162180
raise ValueError(f"Unsupported MCP transport {params['kind']}")
163181
# provide namespace and confirmation control through wrapper class
164-
mcp_servers.append(MCPNamespaceWrap(confirms, mcp_server))
182+
mcp_servers.append((MCPNamespaceWrap(confirms, mcp_server), server_proc))
165183

166184
# connect mcp servers
167185
# https://openai.github.io/openai-agents-python/ref/mcp/server/
@@ -173,22 +191,33 @@ async def mcp_session_task(
173191
# connects/cleanups have to happen in the same task
174192
# but we also want to use wait_for to set a timeout
175193
# so we use a dedicated session task to accomplish both
176-
for server in mcp_servers:
194+
for s in mcp_servers:
195+
server, server_proc = s
177196
logging.debug(f"Connecting mcp server: {server._name}")
197+
if server_proc is not None:
198+
server_proc.start()
199+
await server_proc.async_wait_for_connection(poll_interval=0.1)
178200
await server.connect()
179201
# signal that we're connected
180202
connected.set()
181203
# wait until we're told to clean up
182204
await cleanup.wait()
183-
for server in reversed(mcp_servers):
205+
for s in reversed(mcp_servers):
206+
server, server_proc = s
184207
try:
185208
logging.debug(f"Starting cleanup for mcp server: {server._name}")
186209
await server.cleanup()
187210
logging.debug(f"Cleaned up mcp server: {server._name}")
211+
if server_proc is not None:
212+
server_proc.stop()
213+
try:
214+
await asyncio.to_thread(server_proc.join_and_raise)
215+
except Exception as e:
216+
print(f"Streamable mcp server process exception: {e}")
188217
except asyncio.CancelledError:
189218
logging.error(f"Timeout on cleanup for mcp server: {server._name}")
190219
finally:
191-
mcp_servers.remove(server)
220+
mcp_servers.remove(s)
192221
except RuntimeError as e:
193222
logging.error(f"RuntimeError in mcp session task: {e}")
194223
except asyncio.CancelledError as e:
@@ -233,12 +262,9 @@ async def mcp_session_task(
233262
server_prompts=server_prompts,
234263
important_guidelines=important_guidelines)
235264
),
236-
# XXX: should handoffs have handoffs?
237-
# XXX: this would be a recursive chicken/egg problem :P
238-
# XXX: are initial handoff functions still visible to handoff agents in the run?
239265
handoffs=[],
240266
exclude_from_context=exclude_from_context,
241-
mcp_servers=mcp_servers,
267+
mcp_servers=[s[0] for s in mcp_servers],
242268
model=model,
243269
model_settings=model_settings,
244270
run_hooks=run_hooks,
@@ -257,7 +283,7 @@ async def mcp_session_task(
257283
instructions=prompt_with_handoff_instructions(system_prompt) if handoffs else system_prompt,
258284
handoffs=handoffs,
259285
exclude_from_context=exclude_from_context,
260-
mcp_servers=mcp_servers,
286+
mcp_servers=[s[0] for s in mcp_servers],
261287
model=model,
262288
model_settings=model_settings,
263289
run_hooks=run_hooks,

0 commit comments

Comments
 (0)