voidx 1.0.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.
Files changed (126) hide show
  1. voidx/__init__.py +3 -0
  2. voidx/agent/__init__.py +0 -0
  3. voidx/agent/agents.py +439 -0
  4. voidx/agent/attachments.py +235 -0
  5. voidx/agent/graph.py +463 -0
  6. voidx/agent/graph_components/__init__.py +1 -0
  7. voidx/agent/graph_components/compaction.py +268 -0
  8. voidx/agent/graph_components/permissions.py +139 -0
  9. voidx/agent/graph_components/run_loop.py +532 -0
  10. voidx/agent/graph_components/runtime.py +14 -0
  11. voidx/agent/graph_components/streaming.py +351 -0
  12. voidx/agent/graph_components/subagent.py +278 -0
  13. voidx/agent/graph_components/tool_execution.py +208 -0
  14. voidx/agent/runtime_context.py +368 -0
  15. voidx/agent/slash.py +466 -0
  16. voidx/agent/slash_components/__init__.py +1 -0
  17. voidx/agent/slash_components/code_ide.py +68 -0
  18. voidx/agent/slash_components/lsp.py +105 -0
  19. voidx/agent/slash_components/mcp.py +332 -0
  20. voidx/agent/slash_components/model.py +419 -0
  21. voidx/agent/slash_components/runtime.py +55 -0
  22. voidx/agent/slash_components/skills.py +94 -0
  23. voidx/agent/state.py +32 -0
  24. voidx/agent/task_state.py +278 -0
  25. voidx/agent/tool_filters.py +27 -0
  26. voidx/config.py +707 -0
  27. voidx/llm/__init__.py +0 -0
  28. voidx/llm/catalog.py +188 -0
  29. voidx/llm/compaction.py +267 -0
  30. voidx/llm/context.py +43 -0
  31. voidx/llm/instruction.py +220 -0
  32. voidx/llm/provider.py +312 -0
  33. voidx/llm/usage.py +341 -0
  34. voidx/lsp/__init__.py +30 -0
  35. voidx/lsp/client.py +259 -0
  36. voidx/lsp/config.py +172 -0
  37. voidx/lsp/detector.py +512 -0
  38. voidx/lsp/errors.py +19 -0
  39. voidx/lsp/manager.py +280 -0
  40. voidx/lsp/schema.py +179 -0
  41. voidx/lsp/service.py +103 -0
  42. voidx/main.py +154 -0
  43. voidx/mcp/__init__.py +33 -0
  44. voidx/mcp/client.py +458 -0
  45. voidx/mcp/manager.py +267 -0
  46. voidx/mcp/schema.py +112 -0
  47. voidx/mcp/tool.py +122 -0
  48. voidx/mcp_servers/__init__.py +1 -0
  49. voidx/mcp_servers/web.py +104 -0
  50. voidx/memory/__init__.py +0 -0
  51. voidx/memory/context_frames.py +188 -0
  52. voidx/memory/model_profiles.py +98 -0
  53. voidx/memory/runtime_state.py +240 -0
  54. voidx/memory/session.py +272 -0
  55. voidx/memory/store.py +245 -0
  56. voidx/memory/transcript.py +137 -0
  57. voidx/permission/__init__.py +28 -0
  58. voidx/permission/engine.py +430 -0
  59. voidx/permission/evaluate.py +114 -0
  60. voidx/permission/sandbox.py +280 -0
  61. voidx/permission/schema.py +24 -0
  62. voidx/permission/service.py +314 -0
  63. voidx/permission/wildcard.py +34 -0
  64. voidx/skills/__init__.py +18 -0
  65. voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
  66. voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
  67. voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
  68. voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
  69. voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
  70. voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
  71. voidx/skills/policy.py +97 -0
  72. voidx/skills/registry.py +162 -0
  73. voidx/skills/schema.py +47 -0
  74. voidx/skills/service.py +199 -0
  75. voidx/tools/__init__.py +0 -0
  76. voidx/tools/agent.py +81 -0
  77. voidx/tools/base.py +86 -0
  78. voidx/tools/bash.py +105 -0
  79. voidx/tools/file_ops.py +193 -0
  80. voidx/tools/lsp.py +155 -0
  81. voidx/tools/registry.py +104 -0
  82. voidx/tools/repomap.py +238 -0
  83. voidx/tools/search.py +162 -0
  84. voidx/tools/task_status.py +57 -0
  85. voidx/tools/task_tracker.py +81 -0
  86. voidx/tools/todo.py +82 -0
  87. voidx/tools/web_content.py +357 -0
  88. voidx/tools/web_mcp.py +107 -0
  89. voidx/tools/webfetch.py +155 -0
  90. voidx/tools/websearch.py +276 -0
  91. voidx/ui/__init__.py +0 -0
  92. voidx/ui/app.py +1033 -0
  93. voidx/ui/app_components/__init__.py +1 -0
  94. voidx/ui/app_components/clipboard_image.py +245 -0
  95. voidx/ui/app_components/commands.py +18 -0
  96. voidx/ui/app_components/controls.py +29 -0
  97. voidx/ui/app_components/file_picker.py +115 -0
  98. voidx/ui/app_components/formatting.py +187 -0
  99. voidx/ui/app_components/git_changes.py +51 -0
  100. voidx/ui/app_components/rendering.py +1169 -0
  101. voidx/ui/browse.py +160 -0
  102. voidx/ui/capture.py +169 -0
  103. voidx/ui/code_ide.py +251 -0
  104. voidx/ui/commands.py +83 -0
  105. voidx/ui/console.py +381 -0
  106. voidx/ui/console_components/__init__.py +1 -0
  107. voidx/ui/console_components/formatting.py +96 -0
  108. voidx/ui/console_components/streaming.py +253 -0
  109. voidx/ui/diff.py +331 -0
  110. voidx/ui/dock.py +372 -0
  111. voidx/ui/dock_components/__init__.py +1 -0
  112. voidx/ui/dock_components/formatting.py +123 -0
  113. voidx/ui/dock_components/nodes.py +401 -0
  114. voidx/ui/dock_components/state.py +51 -0
  115. voidx/ui/event_components/__init__.py +1 -0
  116. voidx/ui/event_components/schema.py +249 -0
  117. voidx/ui/events.py +341 -0
  118. voidx/ui/session_changes.py +163 -0
  119. voidx/ui/startup.py +161 -0
  120. voidx/ui/transcript.py +148 -0
  121. voidx/ui/tree.py +316 -0
  122. voidx-1.0.0.dist-info/METADATA +59 -0
  123. voidx-1.0.0.dist-info/RECORD +126 -0
  124. voidx-1.0.0.dist-info/WHEEL +5 -0
  125. voidx-1.0.0.dist-info/entry_points.txt +2 -0
  126. voidx-1.0.0.dist-info/top_level.txt +1 -0
voidx/mcp/manager.py ADDED
@@ -0,0 +1,267 @@
1
+ """MCP manager — lifecycle orchestrator for all MCP server connections.
2
+
3
+ Responsibilities:
4
+ - Reads server configs from Settings
5
+ - Spawns McpClient per server
6
+ - Discovers tools and registers them in ToolRegistry
7
+ - Exposes status for UI
8
+ - Clean shutdown on graph exit
9
+
10
+ Design: all servers start in parallel, a single failure doesn't block others.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import logging
17
+
18
+ from voidx.config import McpServerConfig, Settings
19
+ from voidx.mcp.client import McpClient, McpConnectionError
20
+ from voidx.mcp.schema import McpCallResult, McpRuntimeStatus, McpToolDef
21
+ from voidx.mcp.tool import McpToolWrapper, mcp_tool_id
22
+ from voidx.permission.service import PermissionService
23
+ from voidx.tools.registry import ToolRegistry
24
+
25
+ log = logging.getLogger(__name__)
26
+
27
+
28
+ class McpManager:
29
+ """Manages all MCP server connections for a session."""
30
+
31
+ def __init__(self, settings: Settings | None, registry: ToolRegistry, permission: PermissionService) -> None:
32
+ self._settings = settings
33
+ self._registry = registry
34
+ self._permission = permission
35
+ self._clients: dict[str, McpClient] = {}
36
+ self._started = False
37
+ self._tool_counts: dict[str, int] = {}
38
+ self._errors: dict[str, str] = {}
39
+
40
+ @property
41
+ def started(self) -> bool:
42
+ return self._started
43
+
44
+ # ── lifecycle ───────────────────────────────────────────────────────
45
+
46
+ async def start_all(self) -> None:
47
+ """Start all configured MCP servers and register their tools.
48
+
49
+ Servers start in parallel. A failed server doesn't block others.
50
+ """
51
+ if self._started:
52
+ return
53
+ self._started = True
54
+ self._registry.unregister_prefix("mcp__")
55
+ self._tool_counts.clear()
56
+
57
+ if self._settings is None:
58
+ return
59
+
60
+ servers = self._settings.list_mcp_servers()
61
+ if not servers:
62
+ return
63
+
64
+ enabled = [s for s in servers if not s.disabled]
65
+ if not enabled:
66
+ return
67
+
68
+ log.info("Starting %d MCP server(s)...", len(enabled))
69
+
70
+ # Start all servers concurrently
71
+ results = await self._start_servers(enabled)
72
+
73
+ # Register tools from successfully started servers
74
+ for server_name, client in results:
75
+ try:
76
+ tool_defs = await client.list_tools()
77
+ except Exception as e:
78
+ log.warning("Could not list tools from MCP server '%s': %s", server_name, e)
79
+ self._errors[server_name] = f"Could not list tools: {e}"
80
+ self._tool_counts[server_name] = 0
81
+ continue
82
+ self._errors.pop(server_name, None)
83
+
84
+ allowed = self._resolve_tool_filter(
85
+ next((s for s in servers if s.name == server_name), None)
86
+ )
87
+
88
+ registered = 0
89
+ for td in tool_defs:
90
+ if allowed is not None and td.name not in allowed:
91
+ continue
92
+ wrapper = McpToolWrapper(client, td, server_name)
93
+ self._registry.register(
94
+ wrapper.id,
95
+ wrapper,
96
+ wrapper.description,
97
+ wrapper.parameters_schema(),
98
+ )
99
+ registered += 1
100
+
101
+ # Pre-deny tools that are in deny filter (user explicitly wants them blocked)
102
+ disallowed = self._resolve_tool_filter(
103
+ next((s for s in servers if s.name == server_name), None),
104
+ allow_mode=False,
105
+ )
106
+ if disallowed:
107
+ for tool_name in disallowed:
108
+ tool_id = mcp_tool_id(server_name, tool_name)
109
+ self._permission.deny_silent(tool_id)
110
+
111
+ log.info(
112
+ "MCP server '%s': %d tools registered",
113
+ server_name, registered,
114
+ )
115
+ self._tool_counts[server_name] = registered
116
+
117
+ async def stop_all(self) -> None:
118
+ """Gracefully stop all MCP server connections."""
119
+ self._started = False
120
+ self._registry.unregister_prefix("mcp__")
121
+ self._tool_counts.clear()
122
+ if not self._clients:
123
+ return
124
+
125
+ log.info("Stopping %d MCP server(s)...", len(self._clients))
126
+ await self._stop_servers(list(self._clients.values()))
127
+ self._clients.clear()
128
+
129
+ async def reconnect(self, server_name: str) -> bool:
130
+ """Attempt to reconnect a specific server."""
131
+ client = self._clients.get(server_name)
132
+ if client is None:
133
+ return False
134
+ return await client.reconnect()
135
+
136
+ async def restart_all(self) -> None:
137
+ """Restart all configured server connections and re-register tools."""
138
+ await self.stop_all()
139
+ await self.start_all()
140
+
141
+ async def list_tools_for_server(self, server_name: str) -> list[McpToolDef]:
142
+ """List tools from a connected server."""
143
+ client = self._clients.get(server_name)
144
+ if client is None:
145
+ raise McpConnectionError(f"MCP server '{server_name}' is not connected")
146
+ return await client.list_tools()
147
+
148
+ async def call_tool(
149
+ self,
150
+ server_name: str,
151
+ tool_name: str,
152
+ arguments: dict,
153
+ ) -> McpCallResult:
154
+ """Call a tool on a connected MCP server."""
155
+ client = self._clients.get(server_name)
156
+ if client is None:
157
+ raise McpConnectionError(f"MCP server '{server_name}' is not connected")
158
+ if not client.healthy:
159
+ ok = await client.reconnect()
160
+ if not ok:
161
+ raise McpConnectionError(
162
+ f"MCP server '{server_name}' is unavailable: {client.error_message}"
163
+ )
164
+ return await client.call_tool(tool_name, arguments)
165
+
166
+ def statuses(self) -> list[McpRuntimeStatus]:
167
+ """Return runtime status of all configured servers (for UI)."""
168
+ if self._settings is None:
169
+ return []
170
+
171
+ servers = self._settings.list_mcp_servers()
172
+ result: list[McpRuntimeStatus] = []
173
+ for sc in servers:
174
+ if sc.disabled:
175
+ result.append(McpRuntimeStatus(
176
+ name=sc.name,
177
+ status="disabled",
178
+ ))
179
+ continue
180
+
181
+ client = self._clients.get(sc.name)
182
+ if client is None:
183
+ error = self._errors.get(sc.name, "")
184
+ result.append(McpRuntimeStatus(
185
+ name=sc.name,
186
+ status="error" if error else "disconnected",
187
+ error_message=error,
188
+ ))
189
+ else:
190
+ error = self._errors.get(sc.name, "")
191
+ result.append(McpRuntimeStatus(
192
+ name=sc.name,
193
+ status="error" if error else client.status,
194
+ tool_count=self._tool_counts.get(sc.name, 0),
195
+ error_message=error or client.error_message,
196
+ ))
197
+ return result
198
+
199
+ # ── internal ────────────────────────────────────────────────────────
200
+
201
+ async def _start_servers(self, configs: list[McpServerConfig]) -> list[tuple[str, McpClient]]:
202
+ """Start multiple servers in parallel, collect successes."""
203
+ async def try_start(sc: McpServerConfig) -> tuple[str, McpClient] | None:
204
+ if sc.name in self._clients:
205
+ return sc.name, self._clients[sc.name]
206
+ client = McpClient(sc)
207
+ try:
208
+ await client.start()
209
+ self._clients[sc.name] = client
210
+ self._errors.pop(sc.name, None)
211
+ return sc.name, client
212
+ except McpConnectionError as e:
213
+ log.warning("MCP server '%s' failed to start: %s", sc.name, e)
214
+ self._errors[sc.name] = str(e)
215
+ return None
216
+
217
+ tasks = [try_start(sc) for sc in configs]
218
+ results = await self._gather_safe(tasks)
219
+
220
+ successes: list[tuple[str, McpClient]] = []
221
+ for entry in results:
222
+ if entry is not None and not isinstance(entry, BaseException):
223
+ successes.append(entry)
224
+ return successes
225
+
226
+ async def _stop_servers(self, clients: list[McpClient]) -> None:
227
+ """Stop multiple servers in parallel."""
228
+ async def safe_stop(client: McpClient) -> None:
229
+ try:
230
+ await client.stop()
231
+ except Exception:
232
+ log.exception("Error stopping MCP server '%s'", client.server_name)
233
+
234
+ await self._gather_safe([safe_stop(c) for c in clients])
235
+
236
+ @staticmethod
237
+ async def _gather_safe(tasks: list) -> list:
238
+ results = await asyncio.gather(*tasks, return_exceptions=True)
239
+ return [None if isinstance(r, BaseException) else r for r in results]
240
+
241
+ @staticmethod
242
+ def _resolve_tool_filter(
243
+ server_config: McpServerConfig | None, allow_mode: bool = True,
244
+ ) -> set[str] | None:
245
+ """Resolve the tools filter from server config.
246
+
247
+ If allow_mode=True: returns the set of tool names that ARE allowed.
248
+ If allow_mode=False: returns the set of tool names that are DENIED.
249
+ """
250
+ if server_config is None:
251
+ return None
252
+ tools_field = getattr(server_config, "tools", None)
253
+ if tools_field is None:
254
+ return None
255
+ if isinstance(tools_field, list):
256
+ # List of tool names that are allowed
257
+ if len(tools_field) == 0:
258
+ return set() if allow_mode else None
259
+ return set(tools_field) if allow_mode else None
260
+ if isinstance(tools_field, dict):
261
+ if allow_mode:
262
+ allowed = {k for k, v in tools_field.items() if v}
263
+ return allowed if allowed else None
264
+ else:
265
+ denied = {k for k, v in tools_field.items() if not v}
266
+ return denied if denied else None
267
+ return None
voidx/mcp/schema.py ADDED
@@ -0,0 +1,112 @@
1
+ """MCP protocol types — JSON-RPC wire format, tool definitions, call results.
2
+
3
+ Based on the Model Context Protocol specification:
4
+ https://spec.modelcontextprotocol.io/
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ # ── JSON-RPC 2.0 ──────────────────────────────────────────────────────────
14
+
15
+
16
+ @dataclass
17
+ class JsonRpcRequest:
18
+ """A JSON-RPC 2.0 request message."""
19
+ jsonrpc: str = "2.0"
20
+ id: int = 0
21
+ method: str = ""
22
+ params: dict[str, Any] = field(default_factory=dict)
23
+
24
+ def to_dict(self) -> dict[str, Any]:
25
+ d: dict[str, Any] = {
26
+ "jsonrpc": "2.0",
27
+ "id": self.id,
28
+ "method": self.method,
29
+ }
30
+ if self.params:
31
+ d["params"] = self.params
32
+ return d
33
+
34
+
35
+ @dataclass
36
+ class JsonRpcResponse:
37
+ """A JSON-RPC 2.0 response message."""
38
+ id: int = 0
39
+ result: Any = None
40
+ error: dict[str, Any] | None = None
41
+
42
+
43
+ @dataclass
44
+ class JsonRpcNotification:
45
+ """A JSON-RPC 2.0 notification (no id, no response expected)."""
46
+ jsonrpc: str = "2.0"
47
+ method: str = ""
48
+ params: dict[str, Any] | None = None
49
+
50
+ def to_dict(self) -> dict[str, Any]:
51
+ d: dict[str, Any] = {
52
+ "jsonrpc": "2.0",
53
+ "method": self.method,
54
+ }
55
+ if self.params:
56
+ d["params"] = self.params
57
+ return d
58
+
59
+
60
+ # ── MCP protocol messages ─────────────────────────────────────────────────
61
+
62
+
63
+ MCP_PROTOCOL_VERSION = "2025-03-26"
64
+
65
+
66
+ @dataclass
67
+ class McpInitializeParams:
68
+ """Parameters for the initialize request."""
69
+ protocol_version: str = MCP_PROTOCOL_VERSION
70
+ capabilities: dict[str, Any] = field(default_factory=lambda: {
71
+ "roots": {"listChanged": False},
72
+ "sampling": {},
73
+ })
74
+ client_info: dict[str, str] = field(default_factory=lambda: {
75
+ "name": "voidx",
76
+ "version": "1.0.0",
77
+ })
78
+
79
+ def to_dict(self) -> dict[str, Any]:
80
+ return {
81
+ "protocolVersion": self.protocol_version,
82
+ "capabilities": self.capabilities,
83
+ "clientInfo": self.client_info,
84
+ }
85
+
86
+
87
+ @dataclass
88
+ class McpToolDef:
89
+ """A tool exposed by an MCP server."""
90
+ name: str
91
+ description: str = ""
92
+ inputSchema: dict[str, Any] = field(default_factory=dict)
93
+
94
+
95
+ @dataclass
96
+ class McpCallResult:
97
+ """Result from a tools/call request."""
98
+ content: list[dict[str, Any]] = field(default_factory=list)
99
+ isError: bool = False
100
+ structured_content: Any = None
101
+
102
+
103
+ # ── Server status (for UI consumption) ────────────────────────────────────
104
+
105
+
106
+ @dataclass
107
+ class McpRuntimeStatus:
108
+ """Runtime status of a single MCP server connection."""
109
+ name: str
110
+ status: str # "connected" | "disconnected" | "error"
111
+ tool_count: int = 0
112
+ error_message: str = ""
voidx/mcp/tool.py ADDED
@@ -0,0 +1,122 @@
1
+ """MCP tool wrapper — adapts MCP server tools into voidx's BaseTool interface.
2
+
3
+ Each discovered MCP tool becomes a McpToolWrapper registered in ToolRegistry
4
+ with an LLM-safe id derived from the server and tool name.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import re
12
+ from typing import Any
13
+
14
+ from voidx.mcp.client import McpClient, McpConnectionError, McpProtocolError, McpTimeoutError
15
+ from voidx.mcp.schema import McpCallResult, McpToolDef
16
+ from voidx.tools.base import BaseTool, ToolResult, ToolContext
17
+
18
+
19
+ _LLM_TOOL_NAME_MAX = 64
20
+ _INVALID_TOOL_NAME_CHARS = re.compile(r"[^A-Za-z0-9_-]+")
21
+
22
+
23
+ def mcp_tool_id(server_name: str, tool_name: str) -> str:
24
+ raw = f"mcp__{server_name}__{tool_name}"
25
+ readable = _INVALID_TOOL_NAME_CHARS.sub("_", raw).strip("_") or "mcp_tool"
26
+ digest = hashlib.sha1(f"{server_name}\0{tool_name}".encode("utf-8")).hexdigest()[:8]
27
+ suffix = f"_{digest}"
28
+ if len(readable) + len(suffix) > _LLM_TOOL_NAME_MAX:
29
+ readable = readable[: _LLM_TOOL_NAME_MAX - len(suffix)].rstrip("_-") or "mcp_tool"
30
+ return f"{readable}{suffix}"
31
+
32
+
33
+ class McpToolWrapper(BaseTool):
34
+ """Wraps an MCP server tool as a voidx BaseTool."""
35
+
36
+ def __init__(self, client: McpClient, tool_def: McpToolDef, server_name: str) -> None:
37
+ self._client = client
38
+ self._tool_def = tool_def
39
+ self._server = server_name
40
+
41
+ @property
42
+ def id(self) -> str:
43
+ return mcp_tool_id(self._server, self._tool_def.name)
44
+
45
+ @property
46
+ def description(self) -> str:
47
+ desc = self._tool_def.description or f"MCP tool from {self._server}"
48
+ return f"[MCP:{self._server}] {desc}"
49
+
50
+ def parameters_schema(self) -> dict[str, Any]:
51
+ return self._tool_def.inputSchema or {"type": "object", "properties": {}}
52
+
53
+ async def execute(self, args: dict[str, Any], ctx: ToolContext) -> ToolResult:
54
+ if not self._client.healthy:
55
+ return ToolResult(
56
+ output=f"MCP server '{self._server}' is not connected. "
57
+ f"Status: {self._client.status}."
58
+ f"{' ' + self._client.error_message if self._client.error_message else ''}",
59
+ metadata={"error": True, "server": self._server},
60
+ )
61
+ try:
62
+ result: McpCallResult = await self._client.call_tool(self._tool_def.name, args)
63
+ except (McpConnectionError, McpTimeoutError, McpProtocolError) as e:
64
+ return ToolResult(
65
+ output=f"MCP server '{self._server}' is unavailable: {e}",
66
+ metadata={"error": True, "server": self._server, "error_type": type(e).__name__},
67
+ )
68
+ except Exception as e:
69
+ return ToolResult(
70
+ output=f"MCP tool '{self._tool_def.name}' error: {e}",
71
+ metadata={"error": True, "server": self._server, "error_type": type(e).__name__},
72
+ )
73
+
74
+ output = format_mcp_call_result(result)
75
+ meta: dict[str, Any] = {"server": self._server}
76
+ if result.isError:
77
+ meta["error"] = True
78
+
79
+ return ToolResult(output=output, metadata=meta)
80
+
81
+
82
+ def format_mcp_call_result(result: McpCallResult) -> str:
83
+ parts = [_format_content_block(block) for block in result.content]
84
+ if result.structured_content is not None:
85
+ parts.append(
86
+ "Structured content:\n"
87
+ + json.dumps(result.structured_content, ensure_ascii=False, indent=2, default=str)
88
+ )
89
+ rendered = [part for part in parts if part]
90
+ return "\n".join(rendered) if rendered else "(empty response)"
91
+
92
+
93
+ def _format_content_block(block: Any) -> str:
94
+ if not isinstance(block, dict):
95
+ return str(block) if block is not None else ""
96
+
97
+ block_type = block.get("type", "")
98
+ if block_type == "text":
99
+ text = block.get("text", "")
100
+ return text if isinstance(text, str) else str(text)
101
+
102
+ if block_type == "image":
103
+ mime = block.get("mimeType") or block.get("mime_type") or "unknown"
104
+ data = block.get("data", "")
105
+ size = len(data) if isinstance(data, str) else 0
106
+ return f"[image {mime}, {size} base64 chars]"
107
+
108
+ if block_type == "resource":
109
+ resource = block.get("resource")
110
+ if isinstance(resource, dict):
111
+ uri = resource.get("uri", "resource")
112
+ mime = resource.get("mimeType") or resource.get("mime_type") or ""
113
+ header = f"[resource {uri}{f' ({mime})' if mime else ''}]"
114
+ text = resource.get("text")
115
+ if isinstance(text, str):
116
+ return f"{header}\n{text}"
117
+ blob = resource.get("blob")
118
+ if isinstance(blob, str):
119
+ return f"{header}\n[{len(blob)} base64 chars]"
120
+ return header
121
+
122
+ return json.dumps(block, ensure_ascii=False, sort_keys=True, default=str)
@@ -0,0 +1 @@
1
+ """Built-in MCP servers shipped with voidx."""
@@ -0,0 +1,104 @@
1
+ """Built-in line-delimited JSON-RPC MCP server for web tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import sys
8
+ from typing import Any
9
+
10
+ from voidx.mcp.schema import MCP_PROTOCOL_VERSION
11
+ from voidx.tools.base import BaseTool, ToolContext
12
+ from voidx.tools.webfetch import WebFetchTool
13
+ from voidx.tools.websearch import WebSearchTool
14
+
15
+
16
+ def _tools() -> dict[str, BaseTool]:
17
+ return {
18
+ "web_search": WebSearchTool(settings=None),
19
+ "web_fetch": WebFetchTool(settings=None),
20
+ }
21
+
22
+
23
+ async def _handle(message: dict[str, Any], tools: dict[str, BaseTool]) -> dict[str, Any] | None:
24
+ method = message.get("method")
25
+ msg_id = message.get("id")
26
+
27
+ if method == "initialize":
28
+ params = message.get("params", {})
29
+ protocol_version = params.get("protocolVersion") or MCP_PROTOCOL_VERSION
30
+ return _result(msg_id, {
31
+ "protocolVersion": protocol_version,
32
+ "capabilities": {"tools": {}},
33
+ "serverInfo": {"name": "voidx-web", "version": "1.0.0"},
34
+ })
35
+
36
+ if method == "notifications/initialized":
37
+ return None
38
+
39
+ if method == "tools/list":
40
+ return _result(msg_id, {
41
+ "tools": [
42
+ {
43
+ "name": name,
44
+ "description": tool.description,
45
+ "inputSchema": tool.parameters_schema(),
46
+ }
47
+ for name, tool in tools.items()
48
+ ]
49
+ })
50
+
51
+ if method == "tools/call":
52
+ params = message.get("params", {})
53
+ name = params.get("name", "")
54
+ args = params.get("arguments", {})
55
+ tool = tools.get(name)
56
+ if tool is None:
57
+ return _error(msg_id, -32602, f"Unknown tool: {name}")
58
+ result = await tool.execute(args if isinstance(args, dict) else {}, ToolContext(workspace="."))
59
+ is_error = bool(result.metadata.get("error"))
60
+ return _result(msg_id, {
61
+ "content": [{"type": "text", "text": result.output}],
62
+ "isError": is_error,
63
+ "structuredContent": result.metadata,
64
+ })
65
+
66
+ if method == "shutdown":
67
+ return None
68
+
69
+ if msg_id is None:
70
+ return None
71
+ return _error(msg_id, -32601, f"Method not found: {method}")
72
+
73
+
74
+ def _result(msg_id: Any, result: Any) -> dict[str, Any]:
75
+ return {"jsonrpc": "2.0", "id": msg_id, "result": result}
76
+
77
+
78
+ def _error(msg_id: Any, code: int, message: str) -> dict[str, Any]:
79
+ return {"jsonrpc": "2.0", "id": msg_id, "error": {"code": code, "message": message}}
80
+
81
+
82
+ async def amain() -> None:
83
+ tools = _tools()
84
+ while True:
85
+ raw = await asyncio.to_thread(sys.stdin.readline)
86
+ if not raw:
87
+ break
88
+ try:
89
+ message = json.loads(raw)
90
+ except json.JSONDecodeError:
91
+ continue
92
+ if message.get("method") == "shutdown":
93
+ break
94
+ response = await _handle(message, tools)
95
+ if response is not None:
96
+ print(json.dumps(response, ensure_ascii=False), flush=True)
97
+
98
+
99
+ def main() -> None:
100
+ asyncio.run(amain())
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
File without changes