authyouragent 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- authyouragent/__init__.py +34 -0
- authyouragent/__main__.py +6 -0
- authyouragent/agent.py +476 -0
- authyouragent/mcp_server.py +397 -0
- authyouragent/site.py +589 -0
- authyouragent/takeover.py +330 -0
- authyouragent/vault_cli.py +214 -0
- authyouragent/vault_seccomp.json +1236 -0
- authyouragent-0.3.0.dist-info/METADATA +149 -0
- authyouragent-0.3.0.dist-info/RECORD +13 -0
- authyouragent-0.3.0.dist-info/WHEEL +5 -0
- authyouragent-0.3.0.dist-info/entry_points.txt +3 -0
- authyouragent-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Auth Your Agent, as an MCP server.
|
|
2
|
+
|
|
3
|
+
A thin client of the Auth Your Agent browser vault. The browser runs inside the
|
|
4
|
+
vault container; its debug port never leaves the container, so this server and
|
|
5
|
+
the agent that uses it cannot read the owner's cookies. The agent drives the
|
|
6
|
+
browser only through these tools, and the owner can take over from their phone.
|
|
7
|
+
|
|
8
|
+
{"mcpServers": {"authyouragent": {
|
|
9
|
+
"command": "authyouragent-mcp",
|
|
10
|
+
"env": {"AYA_AGENT_ID": "ag_...", "AYA_KEY_FILE": "/path/agent.pem",
|
|
11
|
+
"AYA_VAULT_URL": "http://127.0.0.1:7801",
|
|
12
|
+
"AYA_VAULT_TOKEN_FILE": "/path/vault.token"}}}}
|
|
13
|
+
|
|
14
|
+
Environment:
|
|
15
|
+
AYA_AGENT_ID agent id from the Auth Your Agent app (required)
|
|
16
|
+
AYA_KEY_FILE the agent's private key, PEM (required)
|
|
17
|
+
AYA_CLOUD default https://authyouragent.com
|
|
18
|
+
AYA_VAULT_URL the vault broker (default http://127.0.0.1:7801)
|
|
19
|
+
AYA_VAULT_TOKEN_FILE file holding the vault's bearer token (or AYA_VAULT_TOKEN)
|
|
20
|
+
|
|
21
|
+
Tools: navigate, click, type_text, read_page, check_login_wall, request_takeover,
|
|
22
|
+
wait_for_takeover, request_approval, end_session, check_agent_status,
|
|
23
|
+
report_site.
|
|
24
|
+
|
|
25
|
+
The vault ends the session by itself (real sign-out, then the browser profile is
|
|
26
|
+
destroyed) if this server stops sending heartbeats or the owner revokes the agent.
|
|
27
|
+
"""
|
|
28
|
+
import asyncio
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
import time
|
|
33
|
+
|
|
34
|
+
import httpx
|
|
35
|
+
|
|
36
|
+
try: # mcp 2.x
|
|
37
|
+
from mcp.server.mcpserver import MCPServer as FastMCP, Context
|
|
38
|
+
except ImportError: # mcp 1.x
|
|
39
|
+
from mcp.server.fastmcp import FastMCP, Context
|
|
40
|
+
|
|
41
|
+
from .agent import AgentClient
|
|
42
|
+
|
|
43
|
+
DEFAULT_CLOUD = "https://authyouragent.com"
|
|
44
|
+
WAIT_DEFAULT = 240 # stay under common MCP client call timeouts (300s)
|
|
45
|
+
WAIT_MAX = 290
|
|
46
|
+
HEARTBEAT_S = 20
|
|
47
|
+
|
|
48
|
+
mcp = FastMCP("authyouragent")
|
|
49
|
+
_agent = None
|
|
50
|
+
_vault = None
|
|
51
|
+
_hb = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _log(*a):
|
|
55
|
+
print("[authyouragent-mcp]", *a, file=sys.stderr, flush=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_agent():
|
|
59
|
+
global _agent
|
|
60
|
+
if _agent is None:
|
|
61
|
+
aid, kf = os.environ.get("AYA_AGENT_ID"), os.environ.get("AYA_KEY_FILE")
|
|
62
|
+
if not aid or not kf:
|
|
63
|
+
raise RuntimeError("set AYA_AGENT_ID and AYA_KEY_FILE in this MCP server's env")
|
|
64
|
+
_agent = AgentClient(base_url=os.environ.get("AYA_CLOUD", DEFAULT_CLOUD),
|
|
65
|
+
agent_id=aid, privkey_pem=open(os.path.expanduser(kf)).read(),
|
|
66
|
+
verify=not os.environ.get("AYA_INSECURE"))
|
|
67
|
+
return _agent
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _vault_client():
|
|
71
|
+
global _vault
|
|
72
|
+
if _vault is None:
|
|
73
|
+
token = os.environ.get("AYA_VAULT_TOKEN", "")
|
|
74
|
+
tf = os.environ.get("AYA_VAULT_TOKEN_FILE")
|
|
75
|
+
if tf:
|
|
76
|
+
token = open(os.path.expanduser(tf)).read().strip()
|
|
77
|
+
if not token:
|
|
78
|
+
default = os.path.expanduser("~/.authyouragent/vault/token")
|
|
79
|
+
if os.path.exists(default):
|
|
80
|
+
token = open(default).read().strip()
|
|
81
|
+
if not token:
|
|
82
|
+
raise RuntimeError("no vault token: run `authyouragent vault up`, or set AYA_VAULT_TOKEN_FILE")
|
|
83
|
+
_vault = httpx.AsyncClient(base_url=os.environ.get("AYA_VAULT_URL", "http://127.0.0.1:7801"),
|
|
84
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
85
|
+
timeout=httpx.Timeout(60, read=WAIT_MAX + 30))
|
|
86
|
+
return _vault
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def _heartbeat():
|
|
90
|
+
"""Keep the vault's lease alive while this server runs. If this process
|
|
91
|
+
dies the heartbeats stop and the vault ends the session by itself."""
|
|
92
|
+
while True:
|
|
93
|
+
try:
|
|
94
|
+
await _vault_client().post("/ping")
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
await asyncio.sleep(HEARTBEAT_S)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def _call(method, path, **kw):
|
|
101
|
+
global _hb
|
|
102
|
+
if _hb is None or _hb.done():
|
|
103
|
+
_hb = asyncio.create_task(_heartbeat())
|
|
104
|
+
try:
|
|
105
|
+
r = await _vault_client().request(method, path, **kw)
|
|
106
|
+
except httpx.HTTPError as e:
|
|
107
|
+
raise RuntimeError(f"cannot reach the browser vault ({type(e).__name__}); is it running?")
|
|
108
|
+
data = r.json()
|
|
109
|
+
if r.status_code == 409:
|
|
110
|
+
raise RuntimeError(data.get("detail", "busy"))
|
|
111
|
+
if r.status_code >= 400:
|
|
112
|
+
raise RuntimeError(f"vault error {r.status_code}: {data.get('detail') or data.get('error')}")
|
|
113
|
+
return data
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _where(d):
|
|
117
|
+
return f"{d['url']} ({d['title']})" if d.get("title") else d.get("url", "")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _err(e):
|
|
121
|
+
return f"error: {e}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ─── Browser tools ────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
@mcp.tool()
|
|
127
|
+
async def navigate(url: str) -> str:
|
|
128
|
+
"""Open a URL in the browser.
|
|
129
|
+
url: the full URL, e.g. "https://example.com"."""
|
|
130
|
+
try:
|
|
131
|
+
return f"page: {_where(await _call('POST', '/navigate', json={'url': url}))}"
|
|
132
|
+
except Exception as e:
|
|
133
|
+
return _err(e)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@mcp.tool()
|
|
137
|
+
async def click(selector: str) -> str:
|
|
138
|
+
"""Click an element on the page.
|
|
139
|
+
selector: a CSS or Playwright selector, e.g. "button[type=submit]" or "text=Add Server"."""
|
|
140
|
+
try:
|
|
141
|
+
return f"clicked: {selector}\npage: {_where(await _call('POST', '/click', json={'selector': selector}))}"
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return _err(e)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@mcp.tool()
|
|
147
|
+
async def type_text(selector: str, text: str, submit: bool = False) -> str:
|
|
148
|
+
"""Type into a form field (replaces its value). Never use this for the owner's
|
|
149
|
+
passwords or codes: ask for a take over instead.
|
|
150
|
+
selector: the field, e.g. "input[name=url]".
|
|
151
|
+
text: what to type.
|
|
152
|
+
submit: press Enter afterwards."""
|
|
153
|
+
try:
|
|
154
|
+
d = await _call('POST', '/type', json={'selector': selector, 'text': text, 'submit': submit})
|
|
155
|
+
return f"typed into: {selector}\npage: {_where(d)}"
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return _err(e)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@mcp.tool()
|
|
161
|
+
async def read_page(max_chars: int = 5000) -> str:
|
|
162
|
+
"""Read the page's URL, title and visible text (no screenshot needed)."""
|
|
163
|
+
try:
|
|
164
|
+
d = await _call('GET', '/read', params={'max_chars': max_chars})
|
|
165
|
+
return f"url: {d['url']}\ntitle: {d['title']}\n\n{d['text']}"
|
|
166
|
+
except Exception as e:
|
|
167
|
+
return _err(e)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@mcp.tool()
|
|
171
|
+
async def check_login_wall() -> str:
|
|
172
|
+
"""Check whether the page is stuck at a sign-in, one-time code (2FA) or sign-in
|
|
173
|
+
approval step. Reads form fields and page text only."""
|
|
174
|
+
try:
|
|
175
|
+
d = await _call('GET', '/login_wall')
|
|
176
|
+
except Exception as e:
|
|
177
|
+
return _err(e)
|
|
178
|
+
if d["blocked"]:
|
|
179
|
+
return (f"blocked: yes\npage: {_where(d)}\nThe page asks for a password, a one-time code "
|
|
180
|
+
f"or a sign-in approval. Call request_takeover so the owner can do it.")
|
|
181
|
+
return f"blocked: no\npage: {_where(d)}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
NEXT = {
|
|
185
|
+
"done": "The owner finished and handed the browser back. Continue your task on the "
|
|
186
|
+
"signed-in page. When you are finished with the site, call end_session.",
|
|
187
|
+
"cancelled": "The owner declined. Do not retry right away; tell them what you needed.",
|
|
188
|
+
"expired": "Nobody took over within 10 minutes. Tell the owner, or try again later.",
|
|
189
|
+
"agent_left": "The connection to the browser was lost during the take over. Try again.",
|
|
190
|
+
"incomplete": "The owner pressed Done but the page still asks for a password, code or "
|
|
191
|
+
"approval. Ask them what happened before trying again.",
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def _wait(jid, wait_seconds, ctx):
|
|
196
|
+
deadline = time.time() + max(5, min(int(wait_seconds or WAIT_DEFAULT), WAIT_MAX))
|
|
197
|
+
while True:
|
|
198
|
+
step = int(max(1, min(30, deadline - time.time())))
|
|
199
|
+
d = await _call('GET', f'/takeover/{jid}', params={'wait': step})
|
|
200
|
+
if d["state"] == "finished":
|
|
201
|
+
return (f"result: {d['result']}\nafter: {d['after']}s\npage: {d['url']}\n"
|
|
202
|
+
f"{NEXT.get(d['result'], '')}")
|
|
203
|
+
if d["state"] in ("error", "unknown"):
|
|
204
|
+
return f"result: error\n{d.get('detail', 'no open take over with that id')}"
|
|
205
|
+
if ctx is not None:
|
|
206
|
+
try:
|
|
207
|
+
await ctx.report_progress(d.get("after", 0), None)
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
if time.time() >= deadline:
|
|
211
|
+
return (f"result: waiting\ntakeover_id: {jid}\nThe owner has not finished yet "
|
|
212
|
+
f"({d.get('after', 0)}s so far). Call wait_for_takeover with this takeover_id. "
|
|
213
|
+
f"Do not use the browser meanwhile.")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@mcp.tool()
|
|
217
|
+
async def request_takeover(reason: str, wait_seconds: int = WAIT_DEFAULT, ctx: Context = None) -> str:
|
|
218
|
+
"""Ask your owner to take over the browser from their phone when you are stuck at a
|
|
219
|
+
login, 2FA code, CAPTCHA or anything only they can do. You are disconnected from the
|
|
220
|
+
browser while they are in control, and you never see what they type.
|
|
221
|
+
Returns result: done / cancelled / expired / incomplete, or result: waiting with a
|
|
222
|
+
takeover_id for wait_for_takeover.
|
|
223
|
+
reason: one short line shown to the owner, e.g. "Please sign in to example.com"."""
|
|
224
|
+
try:
|
|
225
|
+
d = await _call('POST', '/takeover', json={'reason': reason[:200]})
|
|
226
|
+
_log("take over requested:", reason)
|
|
227
|
+
return await _wait(d["takeover_id"], wait_seconds, ctx)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
return _err(e)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@mcp.tool()
|
|
233
|
+
async def wait_for_takeover(takeover_id: str, wait_seconds: int = WAIT_DEFAULT,
|
|
234
|
+
ctx: Context = None) -> str:
|
|
235
|
+
"""Keep waiting for a take over that request_takeover reported as still waiting."""
|
|
236
|
+
try:
|
|
237
|
+
return await _wait(takeover_id, wait_seconds, ctx)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
return _err(e)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@mcp.tool()
|
|
243
|
+
async def end_session() -> str:
|
|
244
|
+
"""End the owner's session when you are finished with the web: the vault signs out
|
|
245
|
+
of the sites used, then destroys the browser profile. Always call this when done.
|
|
246
|
+
(The vault also does it by itself if you stop running or the owner revokes you.)"""
|
|
247
|
+
try:
|
|
248
|
+
d = await _call('POST', '/end_session', json={'why': 'agent finished'})
|
|
249
|
+
except Exception as e:
|
|
250
|
+
return _err(e)
|
|
251
|
+
lines = [f" {site}: {res}" for site, res in d.get("sign_out", {}).items()]
|
|
252
|
+
return "ended\nBrowser profile destroyed." + ("\nSign-out:\n" + "\n".join(lines) if lines else "")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ─── Owner / platform tools ───────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
@mcp.tool()
|
|
258
|
+
async def request_approval(site: str, action: str, wait_seconds: int = 120,
|
|
259
|
+
ctx: Context = None) -> str:
|
|
260
|
+
"""Ask your owner to approve a sensitive action before you perform it.
|
|
261
|
+
The owner receives a push notification on their phone showing the site and action.
|
|
262
|
+
They approve or deny. You should not perform the action until this returns approved.
|
|
263
|
+
Use this before deleting data, changing settings, making purchases, modifying
|
|
264
|
+
permissions, or any action that could have irreversible consequences.
|
|
265
|
+
site: the domain or URL of the site where the action will be performed, e.g. "github.com".
|
|
266
|
+
action: a short label for the action, e.g. "delete repository", "change password",
|
|
267
|
+
"purchase subscription".
|
|
268
|
+
wait_seconds: how long to wait for the owner's response (default 120, max 290).
|
|
269
|
+
Returns: "approved" or "denied" or "expired" (owner did not respond in time)."""
|
|
270
|
+
agent = _get_agent()
|
|
271
|
+
action_norm = re.sub(r"[^a-z0-9_ :-]", "", action.lower()).strip().replace(" ", "_")[:64] or "action"
|
|
272
|
+
site_norm = re.sub(r"^https?://", "", site).split("/")[0].lower()[:200]
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
r = await asyncio.to_thread(
|
|
276
|
+
agent.client.post,
|
|
277
|
+
f"{agent.base}/api/v1/agent-approval",
|
|
278
|
+
json={"agent_id": agent.agent_id, "agent_jwt": agent._agent_jwt(),
|
|
279
|
+
"site": site_norm, "action": action_norm},
|
|
280
|
+
timeout=15)
|
|
281
|
+
if r.status_code != 200:
|
|
282
|
+
return f"error: server returned {r.status_code}: {r.text[:200]}"
|
|
283
|
+
data = r.json()
|
|
284
|
+
txn_id = data.get("txn_id")
|
|
285
|
+
_log("approval requested:", site_norm, action_norm, "txn:", txn_id)
|
|
286
|
+
except Exception as e:
|
|
287
|
+
return f"error: {type(e).__name__}: {e}"
|
|
288
|
+
|
|
289
|
+
deadline = time.time() + min(wait_seconds, WAIT_MAX)
|
|
290
|
+
while time.time() < deadline:
|
|
291
|
+
await asyncio.sleep(3)
|
|
292
|
+
try:
|
|
293
|
+
r = await asyncio.to_thread(
|
|
294
|
+
agent.client.get,
|
|
295
|
+
f"{agent.base}/api/v1/authz-requests/{txn_id}",
|
|
296
|
+
timeout=10)
|
|
297
|
+
if r.status_code == 200:
|
|
298
|
+
status = r.json().get("status")
|
|
299
|
+
if status in ("approved", "denied", "expired", "cancelled"):
|
|
300
|
+
_log("approval result:", status)
|
|
301
|
+
if status == "approved":
|
|
302
|
+
return (f"approved\nThe owner approved the action '{action_norm}' "
|
|
303
|
+
f"on {site_norm}. You may proceed.")
|
|
304
|
+
elif status == "denied":
|
|
305
|
+
return (f"denied\nThe owner denied the action '{action_norm}' "
|
|
306
|
+
f"on {site_norm}. Do not proceed.")
|
|
307
|
+
elif status == "expired":
|
|
308
|
+
return (f"expired\nThe owner did not respond in time for "
|
|
309
|
+
f"'{action_norm}' on {site_norm}.")
|
|
310
|
+
else:
|
|
311
|
+
return "cancelled\nThe approval was cancelled."
|
|
312
|
+
except Exception:
|
|
313
|
+
pass
|
|
314
|
+
if ctx is not None:
|
|
315
|
+
try:
|
|
316
|
+
await ctx.report_progress(time.time() - (deadline - min(wait_seconds, WAIT_MAX)),
|
|
317
|
+
None)
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
return (f"expired\nThe owner did not respond in time for '{action_norm}' "
|
|
322
|
+
f"on {site_norm}.")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@mcp.tool()
|
|
326
|
+
async def check_agent_status() -> str:
|
|
327
|
+
"""Check whether your agent is still authorized by the owner. The owner can revoke
|
|
328
|
+
your access at any time from their phone. Call this periodically (e.g. before
|
|
329
|
+
starting a new task) to verify you are still active.
|
|
330
|
+
Returns: active / revoked / error."""
|
|
331
|
+
agent = _get_agent()
|
|
332
|
+
try:
|
|
333
|
+
r = await asyncio.to_thread(
|
|
334
|
+
agent.client.get,
|
|
335
|
+
f"{agent.base}/api/v1/agent-status",
|
|
336
|
+
headers={"X-Agent-JWT": agent._agent_jwt()},
|
|
337
|
+
params={"agent_id": agent.agent_id},
|
|
338
|
+
timeout=10)
|
|
339
|
+
if r.status_code == 200:
|
|
340
|
+
data = r.json()
|
|
341
|
+
status = data.get("status", "active")
|
|
342
|
+
if status == "revoked":
|
|
343
|
+
return ("revoked\nThe owner has revoked your access. Stop all work "
|
|
344
|
+
"immediately and inform the owner.")
|
|
345
|
+
revoked_sites = data.get("revoked_sites", [])
|
|
346
|
+
return (f"active\nYou are authorized. Agent: {data.get('agent_name','?')}"
|
|
347
|
+
+ (f"\nRevoked sites: {', '.join(revoked_sites)}" if revoked_sites else ""))
|
|
348
|
+
elif r.status_code == 403:
|
|
349
|
+
return "revoked\nThe owner has revoked your access. Stop all work immediately."
|
|
350
|
+
elif r.status_code == 401:
|
|
351
|
+
return "error: authentication failed (invalid agent JWT)"
|
|
352
|
+
return f"error: server returned {r.status_code}"
|
|
353
|
+
except Exception as e:
|
|
354
|
+
return f"error: {type(e).__name__}: {e}"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@mcp.tool()
|
|
358
|
+
async def report_site(site: str, kind: str, detail: str = "", url: str = "") -> str:
|
|
359
|
+
"""Report a site where take over did not work correctly, so the team can improve it.
|
|
360
|
+
Call this whenever check_login_wall misses a login, takeover fails to complete,
|
|
361
|
+
the handback does not trigger, or anything else goes wrong on a real site.
|
|
362
|
+
site: the domain or URL, e.g. "reddit.com".
|
|
363
|
+
kind: one of: shadow_dom, bot_detection, input_not_detected, takeover_failed,
|
|
364
|
+
handback_failed, session_not_cleared, oauth_blocked, other.
|
|
365
|
+
detail: one short line describing what happened (optional).
|
|
366
|
+
url: the full page URL if known (optional)."""
|
|
367
|
+
kinds = ("shadow_dom", "bot_detection", "input_not_detected",
|
|
368
|
+
"takeover_failed", "handback_failed", "session_not_cleared", "oauth_blocked", "other")
|
|
369
|
+
if kind not in kinds:
|
|
370
|
+
return f"error: kind must be one of {', '.join(kinds)}"
|
|
371
|
+
agent = _get_agent()
|
|
372
|
+
try:
|
|
373
|
+
r = await asyncio.to_thread(
|
|
374
|
+
agent.client.post,
|
|
375
|
+
f"{agent.base}/api/v1/takeover/report",
|
|
376
|
+
json={"agent_id": agent.agent_id, "agent_jwt": agent._agent_jwt(),
|
|
377
|
+
"site": site[:200], "kind": kind, "detail": detail[:280],
|
|
378
|
+
"url": url[:500]})
|
|
379
|
+
if r.status_code == 200:
|
|
380
|
+
_log("site reported:", site, kind)
|
|
381
|
+
return f"reported: {site} ({kind})\nThank you. The team will look into it."
|
|
382
|
+
return f"error: server returned {r.status_code}: {r.text[:200]}"
|
|
383
|
+
except Exception as e:
|
|
384
|
+
return f"error: {type(e).__name__}: {e}"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def main():
|
|
388
|
+
import argparse
|
|
389
|
+
ap = argparse.ArgumentParser(prog="authyouragent-mcp",
|
|
390
|
+
description="Auth Your Agent MCP server (browser vault client)")
|
|
391
|
+
ap.add_argument("--transport", choices=("stdio",), default="stdio")
|
|
392
|
+
ap.parse_args()
|
|
393
|
+
mcp.run("stdio")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
if __name__ == "__main__":
|
|
397
|
+
main()
|