agent-portal-2 0.1.0

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.
Files changed (120) hide show
  1. package/.continue/agents/new-config.yaml +22 -0
  2. package/AGENT_STEERING.md +36 -0
  3. package/ARCHITECTURE.md +13 -0
  4. package/CHANGELOG.md +97 -0
  5. package/CLI.md +38 -0
  6. package/CONTRIBUTING.md +55 -0
  7. package/INSTALLATION.md +58 -0
  8. package/LICENSE +60 -0
  9. package/PLUGIN_SYSTEM.md +33 -0
  10. package/PYTHON_SDK.md +22 -0
  11. package/QUICKSTART.md +19 -0
  12. package/README.md +385 -0
  13. package/RELEASE_NOTES_v0.1.0.md +281 -0
  14. package/ROADMAP.md +3 -0
  15. package/RUNTIME.md +44 -0
  16. package/SAFETY_MODEL.md +24 -0
  17. package/TESTING.md +35 -0
  18. package/TROUBLESHOOTING.md +30 -0
  19. package/UPGRADE_GUIDE.md +288 -0
  20. package/VS_CODE_EXTENSION.md +47 -0
  21. package/agent-portal.config.json +20 -0
  22. package/apps/desktop/agent-portal-desktop.zip +0 -0
  23. package/apps/desktop/fixtures/local-workflow.html +151 -0
  24. package/apps/desktop/package.json +18 -0
  25. package/apps/desktop/src/main.ts +117 -0
  26. package/apps/desktop/tsconfig.json +8 -0
  27. package/apps/vscode-extension/LICENSE +60 -0
  28. package/apps/vscode-extension/README.md +20 -0
  29. package/apps/vscode-extension/media/agent-portal-logo.png +0 -0
  30. package/apps/vscode-extension/package.json +149 -0
  31. package/apps/vscode-extension/src/extension.ts +614 -0
  32. package/apps/vscode-extension/tsconfig.json +12 -0
  33. package/assets/branding/agent-portal-logo.png +0 -0
  34. package/connectors/chatgpt-tools/README.md +9 -0
  35. package/connectors/claude-mcp-server/README.md +9 -0
  36. package/connectors/gemini-connector/README.md +9 -0
  37. package/connectors/rest-websocket-api/README.md +9 -0
  38. package/docs/MCP_SERVER.md +68 -0
  39. package/docs/architecture.md +214 -0
  40. package/docs/roadmap.md +125 -0
  41. package/package.json +21 -0
  42. package/packages/agent-portal-mcp/README.md +12 -0
  43. package/packages/agent-portal-mcp/agent_portal_mcp/__init__.py +3 -0
  44. package/packages/agent-portal-mcp/agent_portal_mcp/bridge/__init__.py +1 -0
  45. package/packages/agent-portal-mcp/agent_portal_mcp/bridge/runtime_client.py +180 -0
  46. package/packages/agent-portal-mcp/agent_portal_mcp/cli.py +32 -0
  47. package/packages/agent-portal-mcp/agent_portal_mcp/doctor.py +71 -0
  48. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/__init__.py +1 -0
  49. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/actions.py +17 -0
  50. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/results.py +24 -0
  51. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/risk.py +20 -0
  52. package/packages/agent-portal-mcp/agent_portal_mcp/security/__init__.py +1 -0
  53. package/packages/agent-portal-mcp/agent_portal_mcp/security/policy.py +27 -0
  54. package/packages/agent-portal-mcp/agent_portal_mcp/server.py +148 -0
  55. package/packages/agent-portal-mcp/agent_portal_mcp/tool_registry.py +58 -0
  56. package/packages/agent-portal-mcp/agent_portal_mcp/tools/__init__.py +1 -0
  57. package/packages/agent-portal-mcp/agent_portal_mcp/tools/browser.py +89 -0
  58. package/packages/agent-portal-mcp/agent_portal_mcp/tools/common.py +98 -0
  59. package/packages/agent-portal-mcp/agent_portal_mcp/tools/inspection.py +93 -0
  60. package/packages/agent-portal-mcp/agent_portal_mcp/tools/navigation.py +93 -0
  61. package/packages/agent-portal-mcp/agent_portal_mcp/tools/reports.py +34 -0
  62. package/packages/agent-portal-mcp/agent_portal_mcp/tools/steering.py +93 -0
  63. package/packages/agent-portal-mcp/pyproject.toml +20 -0
  64. package/packages/agent-portal-mcp/tests/test_doctor.py +20 -0
  65. package/packages/agent-portal-mcp/tests/test_mcp_server.py +161 -0
  66. package/packages/core/package.json +15 -0
  67. package/packages/core/src/index.ts +1842 -0
  68. package/packages/core/tsconfig.json +8 -0
  69. package/packages/mcp-server/package.json +15 -0
  70. package/packages/mcp-server/src/index.ts +73 -0
  71. package/packages/mcp-server/tsconfig.json +8 -0
  72. package/packages/sdk/package.json +15 -0
  73. package/packages/sdk/src/index.ts +544 -0
  74. package/packages/sdk/tsconfig.json +8 -0
  75. package/plugins/README.md +16 -0
  76. package/plugins/agent-portal-browser/plugin.json +19 -0
  77. package/plugins/agent-portal-python/plugin.json +16 -0
  78. package/plugins/agent-portal-skills/plugin.json +19 -0
  79. package/plugins/agent-portal-vscode/plugin.json +27 -0
  80. package/plugins/example-runtime-plugin/README.md +3 -0
  81. package/plugins/example-runtime-plugin/plugin.json +20 -0
  82. package/plugins/plugin.schema.json +53 -0
  83. package/python/README.md +18 -0
  84. package/python/agent_portal/__init__.py +5 -0
  85. package/python/agent_portal/__main__.py +5 -0
  86. package/python/agent_portal/browser.py +393 -0
  87. package/python/agent_portal/cli.py +164 -0
  88. package/python/agent_portal/config.py +31 -0
  89. package/python/agent_portal/doctor.py +165 -0
  90. package/python/agent_portal/exceptions.py +39 -0
  91. package/python/agent_portal/logging_utils.py +33 -0
  92. package/python/agent_portal/metrics.py +309 -0
  93. package/python/agent_portal/models.py +160 -0
  94. package/python/agent_portal/plugin_system.py +42 -0
  95. package/python/agent_portal/rate_limit.py +253 -0
  96. package/python/agent_portal/runtime.py +739 -0
  97. package/python/agent_portal/server.py +351 -0
  98. package/python/agent_portal/validation.py +299 -0
  99. package/python/pyproject.toml +29 -0
  100. package/python/tests/test_config.py +24 -0
  101. package/python/tests/test_doctor.py +19 -0
  102. package/python/tests/test_metrics.py +180 -0
  103. package/python/tests/test_rate_limit.py +237 -0
  104. package/python/tests/test_runtime.py +122 -0
  105. package/python/tests/test_server.py +53 -0
  106. package/python/tests/test_validation.py +170 -0
  107. package/releases/desktop/agent-portal-desktop/README.md +378 -0
  108. package/releases/desktop/agent-portal-desktop/RELEASE_NOTES.md +14 -0
  109. package/releases/desktop/agent-portal-desktop/assets/branding/agent-portal-logo.png +0 -0
  110. package/releases/desktop/agent-portal-desktop/fixtures/local-workflow.html +151 -0
  111. package/releases/desktop/agent-portal-desktop/launch-agent-portal.bat +4 -0
  112. package/releases/desktop/agent-portal-desktop.zip +0 -0
  113. package/releases/python/agent_portal-0.0.2-py3-none-any.whl +0 -0
  114. package/releases/python/agent_portal-0.0.2.tar.gz +0 -0
  115. package/scripts/package_desktop.mjs +117 -0
  116. package/scripts/release_python.py +46 -0
  117. package/tests/plugin-manifest.test.mjs +26 -0
  118. package/tests/runtime.test.mjs +41 -0
  119. package/tests/vscode-extension.test.mjs +22 -0
  120. package/tsconfig.base.json +16 -0
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import sys
5
+ from dataclasses import dataclass, field
6
+ from typing import Literal
7
+
8
+ from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient, RuntimeClientError
9
+ from agent_portal_mcp.tool_registry import build_tool_registry
10
+
11
+
12
+ @dataclass(slots=True)
13
+ class DoctorCheck:
14
+ name: str
15
+ status: Literal["passed", "warning", "failed"]
16
+ details: str
17
+ suggested_fix: str | None = None
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class DoctorResult:
22
+ checks: list[DoctorCheck] = field(default_factory=list)
23
+
24
+
25
+ def run_doctor(runtime_url: str = "http://127.0.0.1:8765") -> DoctorResult:
26
+ result = DoctorResult()
27
+ result.checks.append(_check_python_version())
28
+ result.checks.append(_check_package_installed())
29
+ result.checks.append(_check_localhost_binding(runtime_url))
30
+ result.checks.append(_check_tools_registered())
31
+ result.checks.append(_check_runtime(runtime_url))
32
+ return result
33
+
34
+
35
+ def _check_python_version() -> DoctorCheck:
36
+ if sys.version_info >= (3, 10):
37
+ return DoctorCheck("python-version", "passed", sys.version.split()[0])
38
+ return DoctorCheck("python-version", "failed", sys.version.split()[0], "Use Python 3.10 or newer.")
39
+
40
+
41
+ def _check_package_installed() -> DoctorCheck:
42
+ if importlib.util.find_spec("agent_portal_mcp") is not None:
43
+ return DoctorCheck("mcp-package", "passed", "Installed")
44
+ return DoctorCheck("mcp-package", "warning", "Not installed into current environment", "Install with `pip install -e ./packages/agent-portal-mcp`.")
45
+
46
+
47
+ def _check_localhost_binding(runtime_url: str) -> DoctorCheck:
48
+ if runtime_url.startswith("http://127.0.0.1") or runtime_url.startswith("http://localhost"):
49
+ return DoctorCheck("localhost-binding", "passed", runtime_url)
50
+ return DoctorCheck("localhost-binding", "warning", runtime_url, "Prefer localhost-only runtime URLs for MCP usage.")
51
+
52
+
53
+ def _check_tools_registered() -> DoctorCheck:
54
+ count = len(build_tool_registry())
55
+ return DoctorCheck("tool-registry", "passed", f"{count} tools registered")
56
+
57
+
58
+ def _check_runtime(runtime_url: str) -> DoctorCheck:
59
+ client = AgentPortalRuntimeClient(runtime_url=runtime_url)
60
+ try:
61
+ health = client.health()
62
+ except RuntimeClientError:
63
+ return DoctorCheck(
64
+ "runtime-reachable",
65
+ "failed",
66
+ runtime_url,
67
+ "Agent Portal runtime is not running. Start it with: agent-portal start",
68
+ )
69
+
70
+ version = str(health.get("runtimeVersion", "unknown"))
71
+ return DoctorCheck("runtime-reachable", "passed", f"{runtime_url} (runtime {version})")
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from .risk import RiskLevel
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class McpAction:
11
+ tool: str
12
+ runtime_action_type: str
13
+ reason: str
14
+ target: str | None = None
15
+ payload: str | None = None
16
+ risk_hint: RiskLevel = "low"
17
+ metadata: dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from typing import Any
5
+
6
+ from .risk import RiskLevel, ToolStatus
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class ToolResult:
11
+ ok: bool
12
+ tool: str
13
+ session_id: str | None
14
+ action_id: str | None
15
+ status: ToolStatus
16
+ risk: RiskLevel
17
+ message: str
18
+ data: dict[str, Any] = field(default_factory=dict)
19
+ screenshot_before: str | None = None
20
+ screenshot_after: str | None = None
21
+ errors: list[str] = field(default_factory=list)
22
+
23
+ def to_dict(self) -> dict[str, Any]:
24
+ return asdict(self)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+
6
+ RiskLevel = Literal["safe", "low", "medium", "high", "blocked"]
7
+ ToolStatus = Literal[
8
+ "completed",
9
+ "pending_approval",
10
+ "rejected",
11
+ "blocked",
12
+ "failed",
13
+ ]
14
+
15
+
16
+ RISK_ORDER: list[RiskLevel] = ["safe", "low", "medium", "high", "blocked"]
17
+
18
+
19
+ def compare_risk(left: RiskLevel, right: RiskLevel) -> int:
20
+ return RISK_ORDER.index(left) - RISK_ORDER.index(right)
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from agent_portal_mcp.schemas.risk import RiskLevel, compare_risk
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class RuntimePolicySnapshot:
10
+ mode: str = "assisted"
11
+ approval_threshold: RiskLevel = "medium"
12
+ read_only: bool = False
13
+
14
+
15
+ class McpPolicy:
16
+ def should_auto_execute(self, risk: RiskLevel, snapshot: RuntimePolicySnapshot) -> bool:
17
+ if risk == "blocked":
18
+ return False
19
+ if risk == "high":
20
+ return False
21
+ if snapshot.read_only and risk != "safe":
22
+ return False
23
+ if compare_risk(risk, snapshot.approval_threshold) >= 0 and risk != "safe":
24
+ return False
25
+ if snapshot.mode not in {"assisted", "autonomous", "manual-override", "read-only"}:
26
+ return False
27
+ return True
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from dataclasses import asdict
6
+ from typing import Any
7
+
8
+ from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient, RuntimeClientError
9
+ from agent_portal_mcp.schemas.results import ToolResult
10
+ from agent_portal_mcp.tool_registry import ToolDefinition, build_tool_registry
11
+
12
+
13
+ class AgentPortalMcpServer:
14
+ def __init__(self, runtime_url: str = "http://127.0.0.1:8765") -> None:
15
+ self.client = AgentPortalRuntimeClient(runtime_url=runtime_url)
16
+ self.tools = build_tool_registry()
17
+
18
+ def serve_stdio(self) -> None:
19
+ while True:
20
+ payload = self._read_message()
21
+ if payload is None:
22
+ return
23
+ response = self.handle_request(payload)
24
+ if response is not None:
25
+ self._write_message(response)
26
+
27
+ def handle_request(self, payload: dict[str, Any]) -> dict[str, Any] | None:
28
+ method = payload.get("method")
29
+ request_id = payload.get("id")
30
+
31
+ if method == "notifications/initialized":
32
+ return None
33
+ if method == "initialize":
34
+ return self._jsonrpc(
35
+ request_id,
36
+ {
37
+ "protocolVersion": "2024-11-05",
38
+ "capabilities": {"tools": {"listChanged": False}},
39
+ "serverInfo": {"name": "agent-portal-mcp", "version": "0.0.2"},
40
+ },
41
+ )
42
+ if method == "ping":
43
+ return self._jsonrpc(request_id, {})
44
+ if method == "tools/list":
45
+ return self._jsonrpc(
46
+ request_id,
47
+ {
48
+ "tools": [
49
+ {
50
+ "name": tool.name,
51
+ "description": tool.description,
52
+ "inputSchema": tool.input_schema,
53
+ }
54
+ for tool in self.tools.values()
55
+ ]
56
+ },
57
+ )
58
+ if method == "tools/call":
59
+ params = payload.get("params", {})
60
+ name = str(params.get("name", ""))
61
+ arguments = params.get("arguments", {})
62
+ if not isinstance(arguments, dict):
63
+ arguments = {}
64
+ result = self.call_tool(name, arguments)
65
+ return self._jsonrpc(
66
+ request_id,
67
+ {
68
+ "content": [{"type": "text", "text": json.dumps(result.to_dict(), indent=2)}],
69
+ "structuredContent": result.to_dict(),
70
+ "isError": not result.ok,
71
+ },
72
+ )
73
+
74
+ return self._error(request_id, -32601, f"Method not found: {method}")
75
+
76
+ def call_tool(self, name: str, arguments: dict[str, Any]) -> ToolResult:
77
+ definition = self.tools.get(name)
78
+ if definition is None:
79
+ return ToolResult(
80
+ ok=False,
81
+ tool=name,
82
+ session_id=None,
83
+ action_id=None,
84
+ status="failed",
85
+ risk="blocked",
86
+ message=f"Unknown MCP tool: {name}",
87
+ errors=[f"Tool `{name}` is not registered."],
88
+ )
89
+
90
+ try:
91
+ return definition.handler(self.client, arguments)
92
+ except RuntimeClientError as exc:
93
+ return ToolResult(
94
+ ok=False,
95
+ tool=name,
96
+ session_id=None,
97
+ action_id=None,
98
+ status="failed",
99
+ risk="blocked",
100
+ message=str(exc),
101
+ errors=[str(exc)],
102
+ )
103
+ except Exception as exc:
104
+ return ToolResult(
105
+ ok=False,
106
+ tool=name,
107
+ session_id=None,
108
+ action_id=None,
109
+ status="failed",
110
+ risk="blocked",
111
+ message="MCP tool execution failed.",
112
+ errors=[self._redact(str(exc))],
113
+ )
114
+
115
+ def _read_message(self) -> dict[str, Any] | None:
116
+ headers: dict[str, str] = {}
117
+ while True:
118
+ line = sys.stdin.buffer.readline()
119
+ if not line:
120
+ return None
121
+ if line in {b"\r\n", b"\n"}:
122
+ break
123
+ key, value = line.decode("utf8").split(":", 1)
124
+ headers[key.strip().lower()] = value.strip()
125
+
126
+ length = int(headers.get("content-length", "0"))
127
+ if length <= 0:
128
+ return None
129
+ body = sys.stdin.buffer.read(length)
130
+ return json.loads(body.decode("utf8"))
131
+
132
+ def _write_message(self, payload: dict[str, Any]) -> None:
133
+ encoded = json.dumps(payload).encode("utf8")
134
+ sys.stdout.buffer.write(f"Content-Length: {len(encoded)}\r\n\r\n".encode("utf8"))
135
+ sys.stdout.buffer.write(encoded)
136
+ sys.stdout.buffer.flush()
137
+
138
+ def _jsonrpc(self, request_id: Any, result: dict[str, Any]) -> dict[str, Any]:
139
+ return {"jsonrpc": "2.0", "id": request_id, "result": result}
140
+
141
+ def _error(self, request_id: Any, code: int, message: str) -> dict[str, Any]:
142
+ return {"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}}
143
+
144
+ def _redact(self, value: str) -> str:
145
+ token = self.client.token
146
+ if token:
147
+ return value.replace(token, "[REDACTED]")
148
+ return value
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable
5
+
6
+ from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient
7
+ from agent_portal_mcp.schemas.results import ToolResult
8
+ from agent_portal_mcp.tools import browser, inspection, navigation, reports, steering
9
+
10
+
11
+ ToolHandler = Callable[[AgentPortalRuntimeClient, dict[str, Any]], ToolResult]
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class ToolDefinition:
16
+ name: str
17
+ description: str
18
+ input_schema: dict[str, Any]
19
+ handler: ToolHandler
20
+
21
+
22
+ def build_tool_registry() -> dict[str, ToolDefinition]:
23
+ definitions = [
24
+ ToolDefinition("browser_open", "Open a browser URL through Agent Portal.", {"type": "object", "properties": {"url": {"type": "string"}, "reason": {"type": "string"}}, "required": ["url"]}, browser.browser_open),
25
+ ToolDefinition("browser_close", "Close the current browser session.", {"type": "object", "properties": {"reason": {"type": "string"}}}, browser.browser_close),
26
+ ToolDefinition("browser_refresh", "Refresh the current page.", {"type": "object", "properties": {"reason": {"type": "string"}}}, browser.browser_refresh),
27
+ ToolDefinition("browser_back", "Navigate backward in history.", {"type": "object", "properties": {"reason": {"type": "string"}}}, browser.browser_back),
28
+ ToolDefinition("browser_forward", "Navigate forward in history.", {"type": "object", "properties": {"reason": {"type": "string"}}}, browser.browser_forward),
29
+ ToolDefinition("browser_status", "Return browser session status.", {"type": "object", "properties": {}}, browser.browser_status),
30
+ ToolDefinition("navigate_to_url", "Navigate to a URL through the runtime.", {"type": "object", "properties": {"url": {"type": "string"}, "reason": {"type": "string"}}, "required": ["url"]}, navigation.navigate_to_url),
31
+ ToolDefinition("click_element", "Click an element selected by CSS, role text, or label.", {"type": "object", "properties": {"selector": {"type": "string"}, "reason": {"type": "string"}}, "required": ["selector"]}, navigation.click_element),
32
+ ToolDefinition("type_text", "Type text into an input element.", {"type": "object", "properties": {"selector": {"type": "string"}, "text": {"type": "string"}, "reason": {"type": "string"}}, "required": ["selector", "text"]}, navigation.type_text),
33
+ ToolDefinition("scroll_page", "Scroll the page or bring an element into view.", {"type": "object", "properties": {"selector": {"type": "string"}, "reason": {"type": "string"}}}, navigation.scroll_page),
34
+ ToolDefinition("hover_element", "Hover over an element.", {"type": "object", "properties": {"selector": {"type": "string"}, "reason": {"type": "string"}}, "required": ["selector"]}, navigation.hover_element),
35
+ ToolDefinition("wait_for_page", "Wait for a page element to appear.", {"type": "object", "properties": {"selector": {"type": "string"}, "reason": {"type": "string"}}, "required": ["selector"]}, navigation.wait_for_page),
36
+ ToolDefinition("capture_screenshot", "Capture a screenshot with evidence metadata.", {"type": "object", "properties": {"label": {"type": "string"}}}, inspection.capture_screenshot),
37
+ ToolDefinition("read_page_text", "Read visible text from an element.", {"type": "object", "properties": {"selector": {"type": "string"}, "reason": {"type": "string"}}, "required": ["selector"]}, inspection.read_page_text),
38
+ ToolDefinition("read_dom", "Read the current page DOM.", {"type": "object", "properties": {}}, inspection.read_dom),
39
+ ToolDefinition("read_accessibility_tree", "Read the page accessibility tree.", {"type": "object", "properties": {}}, inspection.read_accessibility_tree),
40
+ ToolDefinition("read_console_errors", "Read collected console errors.", {"type": "object", "properties": {}}, inspection.read_console_errors),
41
+ ToolDefinition("read_network_failures", "Read collected network failures.", {"type": "object", "properties": {}}, inspection.read_network_failures),
42
+ ToolDefinition("inspect_element", "Inspect a specific element.", {"type": "object", "properties": {"selector": {"type": "string"}}, "required": ["selector"]}, inspection.inspect_element),
43
+ ToolDefinition("propose_action", "Propose an action through the runtime policy engine.", {"type": "object", "properties": {"action_type": {"type": "string"}, "target": {"type": "string"}, "payload": {"type": "string"}, "reason": {"type": "string"}, "risk": {"type": "string"}}, "required": ["action_type", "reason"]}, steering.propose_action),
44
+ ToolDefinition("approve_action", "Approve and optionally execute a queued action.", {"type": "object", "properties": {"action_id": {"type": "string"}, "execute": {"type": "boolean"}}, "required": ["action_id"]}, steering.approve_action),
45
+ ToolDefinition("reject_action", "Reject a queued action.", {"type": "object", "properties": {"action_id": {"type": "string"}, "reason": {"type": "string"}}, "required": ["action_id"]}, steering.reject_action),
46
+ ToolDefinition("pause_agent", "Pause the agent runtime.", {"type": "object", "properties": {}}, steering.pause_agent),
47
+ ToolDefinition("resume_agent", "Resume the agent runtime.", {"type": "object", "properties": {}}, steering.resume_agent),
48
+ ToolDefinition("stop_agent", "Stop the agent runtime.", {"type": "object", "properties": {}}, steering.stop_agent),
49
+ ToolDefinition("set_goal", "Set the current runtime goal.", {"type": "object", "properties": {"goal": {"type": "string"}}, "required": ["goal"]}, steering.set_goal),
50
+ ToolDefinition("get_current_goal", "Get the current runtime goal.", {"type": "object", "properties": {}}, steering.get_current_goal),
51
+ ToolDefinition("get_action_queue", "Get the runtime action queue.", {"type": "object", "properties": {}}, steering.get_action_queue),
52
+ ToolDefinition("set_action_mode", "Set the runtime action mode.", {"type": "object", "properties": {"mode": {"type": "string"}}, "required": ["mode"]}, steering.set_action_mode),
53
+ ToolDefinition("generate_report", "Generate a runtime report.", {"type": "object", "properties": {}}, reports.generate_report),
54
+ ToolDefinition("list_reports", "List available runtime reports.", {"type": "object", "properties": {}}, reports.list_reports),
55
+ ToolDefinition("read_report", "Read a report by name.", {"type": "object", "properties": {"report": {"type": "string"}}, "required": ["report"]}, reports.read_report),
56
+ ToolDefinition("export_report", "Export a report to a destination path.", {"type": "object", "properties": {"report": {"type": "string"}, "destination": {"type": "string"}}, "required": ["report"]}, reports.export_report),
57
+ ]
58
+ return {definition.name: definition for definition in definitions}
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient
6
+ from agent_portal_mcp.schemas.actions import McpAction
7
+ from agent_portal_mcp.schemas.results import ToolResult
8
+ from agent_portal_mcp.tools.common import build_result, execute_or_queue
9
+
10
+
11
+ def browser_open(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
12
+ return execute_or_queue(
13
+ client,
14
+ "browser_open",
15
+ McpAction(
16
+ tool="browser_open",
17
+ runtime_action_type="open_url",
18
+ reason=str(args.get("reason", "Open browser URL")),
19
+ target=str(args.get("url", "")),
20
+ risk_hint="low",
21
+ ),
22
+ lambda _: {"url": args.get("url")},
23
+ )
24
+
25
+
26
+ def browser_close(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
27
+ return execute_or_queue(
28
+ client,
29
+ "browser_close",
30
+ McpAction(
31
+ tool="browser_close",
32
+ runtime_action_type="browser_close",
33
+ reason=str(args.get("reason", "Close browser session")),
34
+ risk_hint="medium",
35
+ ),
36
+ )
37
+
38
+
39
+ def browser_refresh(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
40
+ return execute_or_queue(
41
+ client,
42
+ "browser_refresh",
43
+ McpAction(
44
+ tool="browser_refresh",
45
+ runtime_action_type="browser_refresh",
46
+ reason=str(args.get("reason", "Refresh current page")),
47
+ risk_hint="low",
48
+ ),
49
+ )
50
+
51
+
52
+ def browser_back(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
53
+ return execute_or_queue(
54
+ client,
55
+ "browser_back",
56
+ McpAction(
57
+ tool="browser_back",
58
+ runtime_action_type="browser_back",
59
+ reason=str(args.get("reason", "Navigate backward")),
60
+ risk_hint="low",
61
+ ),
62
+ )
63
+
64
+
65
+ def browser_forward(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
66
+ return execute_or_queue(
67
+ client,
68
+ "browser_forward",
69
+ McpAction(
70
+ tool="browser_forward",
71
+ runtime_action_type="browser_forward",
72
+ reason=str(args.get("reason", "Navigate forward")),
73
+ risk_hint="low",
74
+ ),
75
+ )
76
+
77
+
78
+ def browser_status(client: AgentPortalRuntimeClient, _args: dict[str, Any]) -> ToolResult:
79
+ status = client.status()
80
+ browser = client.browser_status()
81
+ return build_result(
82
+ "browser_status",
83
+ status,
84
+ None,
85
+ "completed",
86
+ "safe",
87
+ "Returned browser status.",
88
+ {"browser": browser},
89
+ )
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+ from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient
6
+ from agent_portal_mcp.schemas.actions import McpAction
7
+ from agent_portal_mcp.schemas.results import ToolResult
8
+ from agent_portal_mcp.schemas.risk import RiskLevel
9
+ from agent_portal_mcp.security.policy import McpPolicy, RuntimePolicySnapshot
10
+
11
+
12
+ def runtime_policy_snapshot(status_payload: dict[str, Any]) -> RuntimePolicySnapshot:
13
+ policy = status_payload.get("policy", {})
14
+ return RuntimePolicySnapshot(
15
+ mode=str(policy.get("mode", "assisted")),
16
+ approval_threshold=str(policy.get("approval_threshold", "medium")), # type: ignore[arg-type]
17
+ read_only=bool(policy.get("read_only", False)),
18
+ )
19
+
20
+
21
+ def build_result(
22
+ tool: str,
23
+ status_payload: dict[str, Any],
24
+ action_payload: dict[str, Any] | None,
25
+ status: str,
26
+ risk: RiskLevel,
27
+ message: str,
28
+ data: dict[str, Any] | None = None,
29
+ errors: list[str] | None = None,
30
+ ) -> ToolResult:
31
+ session = status_payload.get("session", {})
32
+ return ToolResult(
33
+ ok=status == "completed",
34
+ tool=tool,
35
+ session_id=session.get("session_id"),
36
+ action_id=action_payload.get("action_id") if action_payload else None,
37
+ status=status, # type: ignore[arg-type]
38
+ risk=risk,
39
+ message=message,
40
+ data=data or {},
41
+ screenshot_before=action_payload.get("before_screenshot") if action_payload else None,
42
+ screenshot_after=action_payload.get("after_screenshot") if action_payload else None,
43
+ errors=errors or [],
44
+ )
45
+
46
+
47
+ def execute_or_queue(
48
+ client: AgentPortalRuntimeClient,
49
+ tool: str,
50
+ action: McpAction,
51
+ data_builder: Callable[[dict[str, Any] | None], dict[str, Any]] | None = None,
52
+ ) -> ToolResult:
53
+ status_payload = client.status()
54
+ proposed = client.propose_action(
55
+ action.runtime_action_type,
56
+ action.reason,
57
+ target=action.target,
58
+ payload=action.payload,
59
+ risk_level=action.risk_hint,
60
+ )
61
+ risk = str(proposed.get("risk_level", action.risk_hint)) # type: ignore[assignment]
62
+ policy = McpPolicy()
63
+ snapshot = runtime_policy_snapshot(status_payload)
64
+
65
+ if proposed.get("status") == "blocked":
66
+ return build_result(
67
+ tool,
68
+ status_payload,
69
+ proposed,
70
+ "blocked",
71
+ risk, # type: ignore[arg-type]
72
+ "Action was blocked by the Agent Portal runtime policy engine.",
73
+ data_builder(proposed) if data_builder else {},
74
+ [str(proposed.get("result") or "blocked")],
75
+ )
76
+
77
+ if not policy.should_auto_execute(risk, snapshot): # type: ignore[arg-type]
78
+ return build_result(
79
+ tool,
80
+ status_payload,
81
+ proposed,
82
+ "pending_approval",
83
+ risk, # type: ignore[arg-type]
84
+ "Action entered the approval queue.",
85
+ data_builder(proposed) if data_builder else {},
86
+ )
87
+
88
+ completed = client.approve_action(str(proposed.get("action_id", "")), execute=True)
89
+ fresh_status = client.status()
90
+ return build_result(
91
+ tool,
92
+ fresh_status,
93
+ completed,
94
+ "completed",
95
+ risk, # type: ignore[arg-type]
96
+ "Action executed through the Agent Portal runtime.",
97
+ data_builder(completed) if data_builder else {},
98
+ )
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agent_portal_mcp.bridge.runtime_client import AgentPortalRuntimeClient
6
+ from agent_portal_mcp.schemas.results import ToolResult
7
+ from agent_portal_mcp.tools.common import build_result
8
+
9
+
10
+ def capture_screenshot(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
11
+ status = client.status()
12
+ payload = client.capture(str(args.get("label", "mcp-capture")))
13
+ action = payload.get("action", {})
14
+ return build_result(
15
+ "capture_screenshot",
16
+ status,
17
+ action if isinstance(action, dict) else None,
18
+ "completed",
19
+ "safe",
20
+ "Captured screenshot.",
21
+ payload,
22
+ )
23
+
24
+
25
+ def read_page_text(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
26
+ status = client.status()
27
+ payload = client.read_text(
28
+ str(args.get("selector", "body")),
29
+ str(args.get("reason", "Read page text")),
30
+ )
31
+ return build_result("read_page_text", status, payload.get("action"), "completed", "safe", "Read page text.", payload)
32
+
33
+
34
+ def read_dom(client: AgentPortalRuntimeClient, _args: dict[str, Any]) -> ToolResult:
35
+ status = client.status()
36
+ payload = client.read_dom()
37
+ return build_result("read_dom", status, None, "completed", "safe", "Read page DOM.", payload)
38
+
39
+
40
+ def read_accessibility_tree(client: AgentPortalRuntimeClient, _args: dict[str, Any]) -> ToolResult:
41
+ status = client.status()
42
+ payload = client.read_accessibility_tree()
43
+ return build_result(
44
+ "read_accessibility_tree",
45
+ status,
46
+ None,
47
+ "completed",
48
+ "safe",
49
+ "Read accessibility tree.",
50
+ payload,
51
+ )
52
+
53
+
54
+ def read_console_errors(client: AgentPortalRuntimeClient, _args: dict[str, Any]) -> ToolResult:
55
+ status = client.status()
56
+ payload = client.read_console_errors()
57
+ return build_result(
58
+ "read_console_errors",
59
+ status,
60
+ None,
61
+ "completed",
62
+ "safe",
63
+ "Read console errors.",
64
+ payload,
65
+ )
66
+
67
+
68
+ def read_network_failures(client: AgentPortalRuntimeClient, _args: dict[str, Any]) -> ToolResult:
69
+ status = client.status()
70
+ payload = client.read_network_failures()
71
+ return build_result(
72
+ "read_network_failures",
73
+ status,
74
+ None,
75
+ "completed",
76
+ "safe",
77
+ "Read network failures.",
78
+ payload,
79
+ )
80
+
81
+
82
+ def inspect_element(client: AgentPortalRuntimeClient, args: dict[str, Any]) -> ToolResult:
83
+ status = client.status()
84
+ payload = client.inspect_element(str(args.get("selector", "")))
85
+ return build_result(
86
+ "inspect_element",
87
+ status,
88
+ None,
89
+ "completed",
90
+ "safe",
91
+ "Inspected element.",
92
+ payload,
93
+ )