MCP Server finalizing
frdel committed
May 30, 2025 at 13:48 UTC
aa63ebbb57cbf03a0c10448846575b07c1eb126f
7 files changed
+265
-109
agent.py
+6
-11
@@ -13,7 +13,7 @@ from python.helpers.print_style import PrintStyle
13
from langchain_core.prompts import (
14
ChatPromptTemplate,
15
)
16
-from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage
16
+from langchain_core.messages import HumanMessage, SystemMessage, BaseMessage
17
18
import python.helpers.log as Log
19
from python.helpers.dirty_json import DirtyJson
@@ -121,21 +121,16 @@ class AgentContext:
121
def nudge(self):
122
self.kill_process()
123
self.paused = False
124
- if self.streaming_agent:
125
- current_agent = self.streaming_agent
126
- else:
127
- current_agent = self.agent0
128
-
129
- self.task = self.run_task(current_agent.monologue)
124
+ self.task = self.run_task(self.get_agent().monologue)
125
return self.task
126
127
+ def get_agent(self):
128
+ return self.streaming_agent or self.agent0
129
+
130
def communicate(self, msg: "UserMessage", broadcast_level: int = 1):
131
self.paused = False # unpause if paused
132
135
- if self.streaming_agent:
136
- current_agent = self.streaming_agent
137
- else:
138
- current_agent = self.agent0
133
+ current_agent = self.get_agent()
134
135
if self.task and self.task.is_alive():
136
# set intervention messages to agent(s):
python/helpers/mcp_server.py
+143
-94
@@ -20,21 +20,31 @@ _PRINTER = PrintStyle(italic=True, font_color="green", padding=False)
20
mcp_server: FastMCP = FastMCP(
21
name="Agent Zero integrated MCP Server",
22
instructions="""
23
- This server connects you to the Agent Zero instance running on the remote server.
24
- It exposes tools to interact with the remote Agent Zero instance.
23
+ Connect to remote Agent Zero instance.
24
+ Agent Zero is a general AI assistant controlling it's linux environment.
25
+ Agent Zero can install software, manage files, execute commands, code, use internet, etc.
26
+ Agent Zero's environment is isolated unless configured otherwise.
27
""",
28
)
29
30
31
class ToolResponse(BaseModel):
30
- status: Literal["success"] = Field(description="The status of the response", default="success")
31
- response: str = Field(description="The response from the remote Agent Zero Instance")
32
+ status: Literal["success"] = Field(
33
+ description="The status of the response", default="success"
34
+ )
35
+ response: str = Field(
36
+ description="The response from the remote Agent Zero Instance"
37
+ )
38
chat_id: str = Field(description="The id of the chat this message belongs to.")
39
40
41
class ToolError(BaseModel):
36
- status: Literal["error"] = Field(description="The status of the response", default="error")
37
- error: str = Field(description="The error message from the remote Agent Zero Instance")
42
+ status: Literal["error"] = Field(
43
+ description="The status of the response", default="error"
44
+ )
45
+ error: str = Field(
46
+ description="The error message from the remote Agent Zero Instance"
47
+ )
48
chat_id: str = Field(description="The id of the chat this message belongs to.")
49
50
@@ -47,7 +57,19 @@ This tool is used to send a message to the remote Agent Zero Instance connected
57
@mcp_server.tool(
58
name="send_message",
59
description=SEND_MESSAGE_DESCRIPTION,
50
- tags={"agent_zero", "chat", "remote", "communication", "dialogue", "sse", "send", "message", "start", "new", "continue"},
60
+ tags={
61
+ "agent_zero",
62
+ "chat",
63
+ "remote",
64
+ "communication",
65
+ "dialogue",
66
+ "sse",
67
+ "send",
68
+ "message",
69
+ "start",
70
+ "new",
71
+ "continue",
72
+ },
73
annotations={
74
"remote": True,
75
"readOnlyHint": False,
@@ -58,20 +80,49 @@ This tool is used to send a message to the remote Agent Zero Instance connected
80
},
81
)
82
async def send_message(
61
- message: Annotated[str, Field(description="The message to send to the remote Agent Zero Instance", title="message")],
62
- attachments: Annotated[list[str], Field(
63
- description="Optional: A list of attachments (file paths or web urls) to send to the remote Agent Zero Instance with the message. Default: Empty list",
64
- title="attachments",
65
- )] | None = None,
66
- chat_id: Annotated[str, Field(
67
- description="Optional: ID of the chat. Used to continue a chat. This value is returned in response to sending previous message. Default: Empty string",
68
- title="chat_id",
69
- )] | None = None,
70
- persistent_chat: Annotated[bool, Field(
71
- description="Optional: Whether to use a persistent chat. If true, the chat will be saved and can be continued later. Default: False.",
72
- title="persistent_chat",
73
- )] | None = None,
74
-) -> Annotated[Union[ToolResponse, ToolError], Field(description="The response from the remote Agent Zero Instance", title="response")]:
83
+ message: Annotated[
84
+ str,
85
+ Field(
86
+ description="The message to send to the remote Agent Zero Instance",
87
+ title="message",
88
+ ),
89
+ ],
90
+ attachments: (
91
+ Annotated[
92
+ list[str],
93
+ Field(
94
+ description="Optional: A list of attachments (file paths or web urls) to send to the remote Agent Zero Instance with the message. Default: Empty list",
95
+ title="attachments",
96
+ ),
97
+ ]
98
+ | None
99
+ ) = None,
100
+ chat_id: (
101
+ Annotated[
102
+ str,
103
+ Field(
104
+ description="Optional: ID of the chat. Used to continue a chat. This value is returned in response to sending previous message. Default: Empty string",
105
+ title="chat_id",
106
+ ),
107
+ ]
108
+ | None
109
+ ) = None,
110
+ persistent_chat: (
111
+ Annotated[
112
+ bool,
113
+ Field(
114
+ description="Optional: Whether to use a persistent chat. If true, the chat will be saved and can be continued later. Default: False.",
115
+ title="persistent_chat",
116
+ ),
117
+ ]
118
+ | None
119
+ ) = None,
120
+) -> Annotated[
121
+ Union[ToolResponse, ToolError],
122
+ Field(
123
+ description="The response from the remote Agent Zero Instance", title="response"
124
+ ),
125
+]:
126
context: AgentContext | None = None
127
if chat_id:
128
context = AgentContext.get(chat_id)
@@ -87,7 +138,9 @@ async def send_message(
138
context = AgentContext(config=config, type=AgentContextType.MCP)
139
140
if not message:
90
- return ToolError(error="Message is required", chat_id=context.id if persistent_chat else "")
141
+ return ToolError(
142
+ error="Message is required", chat_id=context.id if persistent_chat else ""
143
+ )
144
145
try:
146
response = await _run_chat(context, message, attachments)
@@ -95,7 +148,9 @@ async def send_message(
148
context.reset()
149
AgentContext.remove(context.id)
150
remove_chat(context.id)
98
- return ToolResponse(response=response, chat_id=context.id if persistent_chat else "")
151
+ return ToolResponse(
152
+ response=response, chat_id=context.id if persistent_chat else ""
153
+ )
154
except Exception as e:
155
return ToolError(error=str(e), chat_id=context.id if persistent_chat else "")
156
@@ -111,7 +166,18 @@ Always use this tool to finish persistent chat conversations with remote Agent Z
166
@mcp_server.tool(
167
name="finish_chat",
168
description=FINISH_CHAT_DESCRIPTION,
114
- tags={"agent_zero", "chat", "remote", "communication", "dialogue", "sse", "finish", "close", "end", "stop"},
169
+ tags={
170
+ "agent_zero",
171
+ "chat",
172
+ "remote",
173
+ "communication",
174
+ "dialogue",
175
+ "sse",
176
+ "finish",
177
+ "close",
178
+ "end",
179
+ "stop",
180
+ },
181
annotations={
182
"remote": True,
183
"readOnlyHint": False,
@@ -122,11 +188,19 @@ Always use this tool to finish persistent chat conversations with remote Agent Z
188
},
189
)
190
async def finish_chat(
125
- chat_id: Annotated[str, Field(
126
- description="ID of the chat to be finished. This value is returned in response to sending previous message.",
127
- title="chat_id",
128
- )]
129
-) -> Annotated[Union[ToolResponse, ToolError], Field(description="The response from the remote Agent Zero Instance", title="response")]:
191
+ chat_id: Annotated[
192
+ str,
193
+ Field(
194
+ description="ID of the chat to be finished. This value is returned in response to sending previous message.",
195
+ title="chat_id",
196
+ ),
197
+ ]
198
+) -> Annotated[
199
+ Union[ToolResponse, ToolError],
200
+ Field(
201
+ description="The response from the remote Agent Zero Instance", title="response"
202
+ ),
203
+]:
204
if not chat_id:
205
return ToolError(error="Chat ID is required", chat_id="")
206
@@ -140,74 +214,49 @@ async def finish_chat(
214
return ToolResponse(response="Chat finished", chat_id=chat_id)
215
216
143
-async def _run_chat(context: AgentContext, message: str, attachments: list[str] | None = None):
144
- async def _run_chat_wrapper(context: AgentContext, message: str, attachments: list[str] | None = None):
145
- # the agent instance - init in try block
146
- agent = None
147
-
148
- try:
149
- _PRINTER.print("MCP Chat message received")
150
-
151
- agent = context.streaming_agent or context.agent0
152
-
153
- # Pcurrent_taskhment filenames for logging
154
- attachment_filenames = []
155
- if attachments:
156
- for attachment in attachments:
157
- if os.path.exists(attachment):
158
- attachment_filenames.append(attachment)
159
- else:
160
- try:
161
- url = urlparse(attachment)
162
- if url.scheme in ["http", "https", "ftp", "ftps", "sftp"]:
163
- attachment_filenames.append(attachment)
164
- else:
165
- _PRINTER.print(f"Skipping attachment: [{attachment}]")
166
- except Exception:
217
+async def _run_chat(
218
+ context: AgentContext, message: str, attachments: list[str] | None = None
219
+):
220
+ try:
221
+ _PRINTER.print("MCP Chat message received")
222
+
223
+ # Pcurrent_taskhment filenames for logging
224
+ attachment_filenames = []
225
+ if attachments:
226
+ for attachment in attachments:
227
+ if os.path.exists(attachment):
228
+ attachment_filenames.append(attachment)
229
+ else:
230
+ try:
231
+ url = urlparse(attachment)
232
+ if url.scheme in ["http", "https", "ftp", "ftps", "sftp"]:
233
+ attachment_filenames.append(attachment)
234
+ else:
235
_PRINTER.print(f"Skipping attachment: [{attachment}]")
168
-
169
- _PRINTER.print("User message:")
170
- _PRINTER.print(f"> {message}")
171
- if attachment_filenames:
172
- _PRINTER.print("Attachments:")
173
- for filename in attachment_filenames:
174
- _PRINTER.print(f"- {filename}")
175
-
176
- # Log the message with message_id and attachments
177
- context.log.log(
178
- type="user",
179
- heading="User message",
180
- content=message,
181
- kvps={"attachments": attachment_filenames},
182
- id=str(uuid.uuid4()),
236
+ except Exception:
237
+ _PRINTER.print(f"Skipping attachment: [{attachment}]")
238
+
239
+ _PRINTER.print("User message:")
240
+ _PRINTER.print(f"> {message}")
241
+ if attachment_filenames:
242
+ _PRINTER.print("Attachments:")
243
+ for filename in attachment_filenames:
244
+ _PRINTER.print(f"- {filename}")
245
+
246
+ task = context.communicate(
247
+ UserMessage(
248
+ message=message, system_message=[], attachments=attachment_filenames
249
)
250
+ )
251
+ result = await task.result()
252
185
- agent.hist_add_user_message(
186
- UserMessage(
187
- message=message,
188
- system_message=[],
189
- attachments=attachment_filenames))
190
-
191
- # Persist after setting up the context but before running the agent
192
- save_tmp_chat(context)
253
+ # Success
254
+ _PRINTER.print(f"MCP Chat message completed: {result}")
255
194
- result = await agent.monologue()
256
+ return result
257
196
- # Success
197
- _PRINTER.print(f"MCP Chat message completed: {result}")
198
- save_tmp_chat(context)
199
-
200
- return result
201
-
202
- except Exception as e:
203
- # Error
204
- _PRINTER.print(f"MCP Chat message failed: {e}")
205
- if agent:
206
- agent.handle_critical_exception(e)
207
-
208
- raise RuntimeError(f"MCP Chat message failed: {e}") from e
258
+ except Exception as e:
259
+ # Error
260
+ _PRINTER.print(f"MCP Chat message failed: {e}")
261
210
- deferred_task = DeferredTask(thread_name="mcp_chat_" + context.id)
211
- deferred_task.start_task(_run_chat_wrapper, context, message, attachments)
212
- asyncio.create_task(asyncio.sleep(0.1)) # Ensure background execution doesn't exit immediately on async await
213
- return await deferred_task.result()
262
+ raise RuntimeError(f"MCP Chat message failed: {e}") from e
python/helpers/settings.py
+27
@@ -62,6 +62,9 @@ class Settings(TypedDict):
62
stt_silence_duration: int
63
stt_waiting_timeout: int
64
65
+ mcp_server_enabled: bool
66
+
67
+
68
69
class PartialSettings(Settings, total=False):
70
pass
@@ -677,6 +680,28 @@ def convert_out(settings: Settings) -> SettingsOutput:
680
"tab": "agent",
681
}
682
683
+ # MCP section
684
+ mcp_server_fields: list[SettingsField] = []
685
+
686
+ mcp_server_fields.append(
687
+ {
688
+ "id": "mcp_server_enabled",
689
+ "title": "Enable A0 MCP Server",
690
+ "description": "Expose Agent Zero as an SSE MCP server. This will make this A0 instance available to MCP clients.",
691
+ "type": "switch",
692
+ "value": settings["mcp_server_enabled"],
693
+ }
694
+ )
695
+
696
+ mcp_server_section: SettingsSection = {
697
+ "id": "mcp_server",
698
+ "title": "A0 MCP Server",
699
+ "description": "Agent Zero can be exposed as an SSE MCP server. It can then be accessed by MCP clients on the URL and port of the web UI + /mcp/sse, for example http://localhost:5000/mcp/sse. The same applies to public URL using Cloudflare Tunnel.",
700
+ "fields": mcp_server_fields,
701
+ "tab": "mcp",
702
+ }
703
+
704
+
705
# Add the section to the result
706
result: SettingsOutput = {
707
"sections": [
@@ -689,6 +714,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
714
stt_section,
715
api_keys_section,
716
auth_section,
717
+ mcp_server_section,
718
dev_section,
719
]
720
}
@@ -839,6 +865,7 @@ def get_default_settings() -> Settings:
865
stt_silence_threshold=0.3,
866
stt_silence_duration=1000,
867
stt_waiting_timeout=2000,
868
+ mcp_server_enabled=False,
869
)
870
871
run_ui.py
+23
-4
@@ -9,7 +9,7 @@ import threading
9
import signal
10
from flask import Flask, request, Response
11
from flask_basicauth import BasicAuth
12
-from python.helpers import errors, files, git
12
+from python.helpers import errors, files, git, settings
13
from python.helpers.files import get_abs_path
14
from python.helpers import persist_chat, runtime, dotenv, process
15
from python.helpers.cloudflare_tunnel import CloudflareTunnel
@@ -19,6 +19,9 @@ from python.helpers.job_loop import run_loop
19
from python.helpers.print_style import PrintStyle
20
from python.helpers.task_scheduler import TaskScheduler
21
from python.helpers.defer import DeferredTask
22
+from starlette.middleware import Middleware
23
+from starlette.middleware.base import BaseHTTPMiddleware
24
+from starlette.exceptions import HTTPException as StarletteHTTPException
25
26
27
# Set the new timezone to 'UTC'
@@ -234,6 +237,22 @@ def run():
237
for handler in handlers:
238
register_api_handler(webapp, handler)
239
240
+
241
+ # define a Starlette-compatible middleware handler
242
+
243
+
244
+ async def mcp_middleware(request, call_next):
245
+ set = settings.get_settings()
246
+ if not set["mcp_server_enabled"]:
247
+ # raise a proper Starlette HTTPException with a clear message
248
+ PrintStyle.error("[MCP] Access denied: MCP server is disabled in settings.")
249
+ raise StarletteHTTPException(status_code=403, detail="MCP server is disabled in settings.")
250
+ return await call_next(request)
251
+
252
+ mcp_middlewares = [
253
+ Middleware(BaseHTTPMiddleware, dispatch=mcp_middleware)
254
+ ]
255
+
256
mcp_app = create_sse_app(
257
server=mcp_server_instance,
258
message_path=mcp_server_instance.settings.message_path,
@@ -242,13 +261,13 @@ def run():
261
auth_settings=mcp_server_instance.settings.auth,
262
debug=mcp_server_instance.settings.debug,
263
routes=mcp_server_instance._additional_http_routes,
245
- middleware=None
264
+ middleware=mcp_middlewares
265
)
266
267
# add the webapp and mcp to the app
268
app = DispatcherMiddleware(webapp, {
250
- "/mcp": ASGIMiddleware(app=mcp_app),
251
- })
269
+ "/mcp": ASGIMiddleware(app=mcp_app), # type: ignore
270
+ }) # type: ignore
271
PrintStyle().debug("Registered middleware for MCP")
272
273
try:
webui/index.html
+4
@@ -583,6 +583,10 @@
583
:class="{'active': activeTab === 'external'}"
584
@click="switchTab('external')"
585
title="External Services">External Services</div>
586
+ <div class="settings-tab"
587
+ :class="{'active': activeTab === 'mcp'}"
588
+ @click="switchTab('mcp')"
589
+ title="MCP">MCP</div>
590
<div class="settings-tab"
591
:class="{'active': activeTab === 'developer'}"
592
@click="switchTab('developer')"
webui/public/mcp_client.svg
new
+35
@@ -0,0 +1,35 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
+<svg width="100%" height="100%" viewBox="0 0 23 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
4
+ <g transform="matrix(-1,0,0,1,22.6247,0)">
5
+ <g>
6
+ <g transform="matrix(0.940988,-0.940988,1.34929,1.34929,-15.8352,15.6937)">
7
+ <path d="M13.071,10.55L12.45,10.116C11.934,9.757 11.934,9.172 12.45,8.812L17.6,5.22C18.116,4.86 18.954,4.86 19.47,5.22L20.2,5.729L20.2,5.042C20.2,4.533 19.607,4.12 18.877,4.12L12.303,4.12C11.573,4.12 10.98,4.533 10.98,5.042L10.98,9.628C10.98,10.137 11.573,10.55 12.303,10.55L13.071,10.55Z" style="fill:none;stroke:black;stroke-width:0.3px;"/>
8
+ </g>
9
+ <g transform="matrix(1.52599,-1.29875e-16,1.12508e-16,2.05479,-8.98544,-4.04941)">
10
+ <path d="M12.9,10.55L19.047,10.55C19.683,10.55 20.2,10.166 20.2,9.693L20.2,4.977C20.2,4.504 19.683,4.12 19.047,4.12L13.033,4.12" style="fill:none;stroke:black;stroke-width:0.28px;"/>
11
+ </g>
12
+ <g transform="matrix(1.52599,-1.29875e-16,1.12508e-16,2.05479,-8.98544,-4.04941)">
13
+ <path d="M13.033,4.12L12.133,4.12C11.497,4.12 10.98,4.504 10.98,4.977L10.98,9.693C10.98,10.166 11.497,10.55 12.133,10.55L12.9,10.55" style="fill:none;stroke:black;stroke-width:0.28px;"/>
14
+ </g>
15
+ <g transform="matrix(1,0,0,1,-1.40112,-0.184252)">
16
+ <circle cx="5.735" cy="11.144" r="1.289"/>
17
+ </g>
18
+ <g transform="matrix(0.44605,0,0,1,2.96694,0)">
19
+ <path d="M3.717,10.959L9.898,10.959" style="fill:none;stroke:black;stroke-width:0.65px;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:1.5;"/>
20
+ </g>
21
+ <g transform="matrix(0.707107,0.707107,-0.707107,0.707107,9.57979,-6.31316)">
22
+ <rect x="9.991" y="6.37" width="3.843" height="3.843"/>
23
+ </g>
24
+ <g transform="matrix(1,0,0,1,-1.72131,-1.70023)">
25
+ <path d="M20.015,13.06L20.942,14.516L22.613,14.948L21.514,16.279L21.621,18.001L20.015,17.368L18.41,18.001L18.516,16.279L17.417,14.948L19.089,14.516L20.015,13.06Z"/>
26
+ </g>
27
+ <g transform="matrix(1,0,0,1,-0.801743,-0.545185)">
28
+ <circle cx="19.021" cy="8.675" r="2.364"/>
29
+ </g>
30
+ <g transform="matrix(1,0,0,1,0.0535086,0.357019)">
31
+ <path d="M14.652,12.568C14.652,12.099 14.271,11.719 13.802,11.719L10.647,11.719C10.178,11.719 9.797,12.099 9.797,12.568L9.797,14.267C9.797,14.736 10.178,15.117 10.647,15.117L11.375,15.117L10.768,16.573L12.103,15.117L13.802,15.117C14.271,15.117 14.652,14.736 14.652,14.267L14.652,12.568Z"/>
32
+ </g>
33
+ </g>
34
+ </g>
35
+</svg>
webui/public/mcp_server.svg
new
+27
@@ -0,0 +1,27 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
+<svg width="100%" height="100%" viewBox="0 0 23 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
4
+ <g>
5
+ <g transform="matrix(0.0161395,0,0,0.0161395,7.32574,2.96434)">
6
+ <g>
7
+ <path d="M717.77,788.27C638.99,652.89 559.87,516.92 479.15,378.22C399.29,516.59 320.58,652.95 241.99,789.12L120,789.12C239.91,581.87 479.49,170.89 479.49,170.89C479.49,170.89 720.12,580.92 840,788.27L717.77,788.27Z" style="fill-rule:nonzero;stroke:rgb(122,122,122);stroke-width:1px;"/>
8
+ <path d="M633.08,788.85L323.54,788.85C344.15,753.01 364.09,718.33 383.88,683.93L574.1,683.93C593.38,718.23 612.57,752.36 633.08,788.85Z" style="fill-rule:nonzero;stroke:rgb(122,122,122);stroke-width:1px;"/>
9
+ </g>
10
+ </g>
11
+ <g transform="matrix(0.940988,-0.940988,1.34929,1.34929,-15.8352,15.6937)">
12
+ <path d="M13.071,10.55L12.45,10.116C11.934,9.757 11.934,9.172 12.45,8.812L17.6,5.22C18.116,4.86 18.954,4.86 19.47,5.22L20.2,5.729L20.2,5.042C20.2,4.533 19.607,4.12 18.877,4.12L12.303,4.12C11.573,4.12 10.98,4.533 10.98,5.042L10.98,9.628C10.98,10.137 11.573,10.55 12.303,10.55L13.071,10.55Z" style="fill:none;stroke:black;stroke-width:0.3px;stroke-linejoin:miter;stroke-miterlimit:10;"/>
13
+ </g>
14
+ <g transform="matrix(1.52599,-1.29875e-16,1.12508e-16,2.05479,-8.98544,-4.04941)">
15
+ <path d="M12.9,10.55L19.047,10.55C19.683,10.55 20.2,10.166 20.2,9.693L20.2,4.977C20.2,4.504 19.683,4.12 19.047,4.12L13.033,4.12" style="fill:none;stroke:black;stroke-width:0.28px;stroke-linejoin:miter;stroke-miterlimit:10;"/>
16
+ </g>
17
+ <g transform="matrix(1.52599,-1.29875e-16,1.12508e-16,2.05479,-8.98544,-4.04941)">
18
+ <path d="M13.033,4.12L12.133,4.12C11.497,4.12 10.98,4.504 10.98,4.977L10.98,9.693C10.98,10.166 11.497,10.55 12.133,10.55L12.9,10.55" style="fill:none;stroke:black;stroke-width:0.28px;stroke-linejoin:miter;stroke-miterlimit:10;"/>
19
+ </g>
20
+ <g transform="matrix(1,0,0,1,-1.40112,-0.184252)">
21
+ <circle cx="5.735" cy="11.144" r="1.289"/>
22
+ </g>
23
+ <g transform="matrix(0.501653,0,0,1,2.76026,0)">
24
+ <path d="M3.717,10.959L9.898,10.959" style="fill:none;stroke:black;stroke-width:0.65px;stroke-miterlimit:1.5;"/>
25
+ </g>
26
+ </g>
27
+</svg>