windcode 0.1.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.
- windcode/__init__.py +6 -0
- windcode/__main__.py +4 -0
- windcode/auth/__init__.py +11 -0
- windcode/auth/store.py +90 -0
- windcode/cli.py +181 -0
- windcode/config/__init__.py +41 -0
- windcode/config/loader.py +117 -0
- windcode/config/models.py +203 -0
- windcode/config/writer.py +74 -0
- windcode/context/__init__.py +18 -0
- windcode/context/compactor.py +72 -0
- windcode/context/estimator.py +89 -0
- windcode/context/truncation.py +87 -0
- windcode/domain/__init__.py +1 -0
- windcode/domain/errors.py +33 -0
- windcode/domain/events.py +597 -0
- windcode/domain/messages.py +226 -0
- windcode/domain/models.py +73 -0
- windcode/domain/subagents.py +263 -0
- windcode/domain/tools.py +55 -0
- windcode/extensions/__init__.py +27 -0
- windcode/extensions/commands.py +25 -0
- windcode/extensions/discovery.py +139 -0
- windcode/extensions/events.py +58 -0
- windcode/extensions/hooks/__init__.py +4 -0
- windcode/extensions/hooks/dispatcher.py +107 -0
- windcode/extensions/hooks/executor.py +49 -0
- windcode/extensions/hooks/loader.py +101 -0
- windcode/extensions/hooks/models.py +109 -0
- windcode/extensions/mcp/__init__.py +10 -0
- windcode/extensions/mcp/adapter.py +101 -0
- windcode/extensions/mcp/catalog.py +153 -0
- windcode/extensions/mcp/client.py +273 -0
- windcode/extensions/mcp/runtime.py +144 -0
- windcode/extensions/mcp/tools.py +570 -0
- windcode/extensions/models.py +168 -0
- windcode/extensions/paths.py +71 -0
- windcode/extensions/plugins/__init__.py +3 -0
- windcode/extensions/plugins/installer.py +93 -0
- windcode/extensions/plugins/manifest.py +166 -0
- windcode/extensions/runtime.py +366 -0
- windcode/extensions/service.py +437 -0
- windcode/extensions/skills/__init__.py +3 -0
- windcode/extensions/skills/loader.py +67 -0
- windcode/extensions/skills/parser.py +57 -0
- windcode/extensions/skills/tools.py +213 -0
- windcode/extensions/snapshot.py +57 -0
- windcode/extensions/state.py +158 -0
- windcode/instructions/__init__.py +8 -0
- windcode/instructions/loader.py +50 -0
- windcode/memory/__init__.py +57 -0
- windcode/memory/extraction.py +83 -0
- windcode/memory/models.py +218 -0
- windcode/memory/refiner.py +189 -0
- windcode/memory/security.py +23 -0
- windcode/memory/service.py +179 -0
- windcode/memory/store.py +362 -0
- windcode/observability/__init__.py +4 -0
- windcode/observability/redaction.py +95 -0
- windcode/observability/trace.py +115 -0
- windcode/policy/__init__.py +22 -0
- windcode/policy/engine.py +137 -0
- windcode/policy/models.py +69 -0
- windcode/providers/__init__.py +29 -0
- windcode/providers/_utils.py +19 -0
- windcode/providers/anthropic.py +215 -0
- windcode/providers/base.py +46 -0
- windcode/providers/catalog.py +119 -0
- windcode/providers/errors.py +96 -0
- windcode/providers/openai_compat.py +231 -0
- windcode/providers/openai_responses.py +189 -0
- windcode/providers/registry.py +105 -0
- windcode/runtime/__init__.py +15 -0
- windcode/runtime/control.py +89 -0
- windcode/runtime/event_bus.py +67 -0
- windcode/runtime/loop.py +510 -0
- windcode/runtime/prompts.py +132 -0
- windcode/runtime/report.py +64 -0
- windcode/runtime/retry.py +73 -0
- windcode/runtime/scheduler.py +194 -0
- windcode/runtime/subagents/__init__.py +28 -0
- windcode/runtime/subagents/approvals.py +88 -0
- windcode/runtime/subagents/budgets.py +79 -0
- windcode/runtime/subagents/coordinator.py +714 -0
- windcode/runtime/subagents/factory.py +311 -0
- windcode/runtime/subagents/roles.py +74 -0
- windcode/runtime/subagents/verification.py +53 -0
- windcode/sandbox/__init__.py +3 -0
- windcode/sandbox/bwrap.py +79 -0
- windcode/sdk.py +1259 -0
- windcode/sessions/__init__.py +23 -0
- windcode/sessions/artifacts.py +69 -0
- windcode/sessions/models.py +104 -0
- windcode/sessions/store.py +167 -0
- windcode/sessions/tree.py +35 -0
- windcode/tools/__init__.py +10 -0
- windcode/tools/apply_patch.py +191 -0
- windcode/tools/ask_user.py +58 -0
- windcode/tools/builtins.py +44 -0
- windcode/tools/edit_file.py +54 -0
- windcode/tools/filesystem.py +77 -0
- windcode/tools/glob.py +35 -0
- windcode/tools/grep.py +66 -0
- windcode/tools/memory.py +400 -0
- windcode/tools/read_file.py +54 -0
- windcode/tools/registry.py +120 -0
- windcode/tools/shell.py +159 -0
- windcode/tools/subagents/__init__.py +31 -0
- windcode/tools/subagents/cancel.py +35 -0
- windcode/tools/subagents/integrate.py +54 -0
- windcode/tools/subagents/list.py +46 -0
- windcode/tools/subagents/spawn.py +86 -0
- windcode/tools/subagents/wait.py +74 -0
- windcode/tools/write_file.py +61 -0
- windcode/tui/__init__.py +3 -0
- windcode/tui/app.py +1015 -0
- windcode/tui/commands.py +90 -0
- windcode/tui/permission_display.py +24 -0
- windcode/tui/styles.tcss +591 -0
- windcode/tui/widgets/__init__.py +31 -0
- windcode/tui/widgets/approval.py +98 -0
- windcode/tui/widgets/command_menu.py +90 -0
- windcode/tui/widgets/extensions.py +48 -0
- windcode/tui/widgets/input.py +110 -0
- windcode/tui/widgets/memory.py +143 -0
- windcode/tui/widgets/messages.py +299 -0
- windcode/tui/widgets/models.py +565 -0
- windcode/tui/widgets/question.py +46 -0
- windcode/tui/widgets/sessions.py +68 -0
- windcode/tui/widgets/status.py +46 -0
- windcode/tui/widgets/subagents.py +195 -0
- windcode/tui/widgets/tools.py +66 -0
- windcode/tui/widgets/welcome.py +149 -0
- windcode/types.py +80 -0
- windcode/worktrees/__init__.py +24 -0
- windcode/worktrees/git.py +92 -0
- windcode/worktrees/manager.py +265 -0
- windcode/worktrees/models.py +62 -0
- windcode-0.1.0.dist-info/METADATA +207 -0
- windcode-0.1.0.dist-info/RECORD +143 -0
- windcode-0.1.0.dist-info/WHEEL +4 -0
- windcode-0.1.0.dist-info/entry_points.txt +2 -0
- windcode-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from mcp.types import (
|
|
9
|
+
ListPromptsResult,
|
|
10
|
+
ListResourcesResult,
|
|
11
|
+
ListResourceTemplatesResult,
|
|
12
|
+
ListToolsResult,
|
|
13
|
+
Prompt,
|
|
14
|
+
Resource,
|
|
15
|
+
ResourceTemplate,
|
|
16
|
+
TextResourceContents,
|
|
17
|
+
Tool,
|
|
18
|
+
)
|
|
19
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
20
|
+
|
|
21
|
+
from windcode.domain.messages import SourcedContextMessage
|
|
22
|
+
from windcode.domain.tools import ToolContext, ToolEffect, ToolResult
|
|
23
|
+
from windcode.extensions.mcp.adapter import McpToolAdapter
|
|
24
|
+
from windcode.extensions.mcp.catalog import (
|
|
25
|
+
McpCatalog,
|
|
26
|
+
McpPromptDefinition,
|
|
27
|
+
McpResourceDefinition,
|
|
28
|
+
McpToolDefinition,
|
|
29
|
+
build_catalog,
|
|
30
|
+
)
|
|
31
|
+
from windcode.extensions.mcp.runtime import McpRuntime
|
|
32
|
+
from windcode.extensions.models import CapabilityKind, CapabilityRecord
|
|
33
|
+
from windcode.sessions.artifacts import ArtifactStore
|
|
34
|
+
from windcode.tools.registry import ToolRegistry
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class McpToolSearchResult:
|
|
39
|
+
stable_id: str
|
|
40
|
+
server_id: str
|
|
41
|
+
name: str
|
|
42
|
+
description: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class McpToolView:
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
catalogs: tuple[McpCatalog, ...],
|
|
49
|
+
adapters: dict[str, McpToolAdapter],
|
|
50
|
+
*,
|
|
51
|
+
direct_tool_limit: int,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._definitions = {tool.stable_id: tool for catalog in catalogs for tool in catalog.tools}
|
|
54
|
+
self._adapters = dict(adapters)
|
|
55
|
+
self.direct_tool_limit = direct_tool_limit
|
|
56
|
+
self._selected: set[str] = set()
|
|
57
|
+
|
|
58
|
+
def search(self, query: str = "") -> tuple[McpToolSearchResult, ...]:
|
|
59
|
+
needle = query.casefold().strip()
|
|
60
|
+
return tuple(
|
|
61
|
+
McpToolSearchResult(item.stable_id, item.server_id, item.name, item.description)
|
|
62
|
+
for item in sorted(self._definitions.values(), key=lambda value: value.stable_id)
|
|
63
|
+
if not needle or needle in item.name.casefold() or needle in item.description.casefold()
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def select(self, stable_id: str) -> McpToolDefinition:
|
|
67
|
+
try:
|
|
68
|
+
definition = self._definitions[stable_id.removeprefix("select:")]
|
|
69
|
+
except KeyError as exc:
|
|
70
|
+
raise KeyError(f"unknown MCP tool: {stable_id}") from exc
|
|
71
|
+
self._selected.add(definition.stable_id)
|
|
72
|
+
return definition
|
|
73
|
+
|
|
74
|
+
def register_visible(self, registry: ToolRegistry) -> None:
|
|
75
|
+
visible = (
|
|
76
|
+
set(self._definitions)
|
|
77
|
+
if len(self._definitions) <= self.direct_tool_limit
|
|
78
|
+
else self._selected
|
|
79
|
+
)
|
|
80
|
+
for stable_id in sorted(visible):
|
|
81
|
+
registry.register(self._adapters[stable_id], replace=True)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True, slots=True)
|
|
85
|
+
class SourcedMcpContent:
|
|
86
|
+
server_id: str
|
|
87
|
+
identity: str
|
|
88
|
+
content: str
|
|
89
|
+
artifact_ref: str | None = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class McpCapabilityService:
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
runtime: McpRuntime,
|
|
96
|
+
*,
|
|
97
|
+
artifact_store: ArtifactStore | None = None,
|
|
98
|
+
content_limit: int = 20_000,
|
|
99
|
+
tool_catalogs: dict[str, tuple[McpToolDefinition, ...]] | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
self.runtime = runtime
|
|
102
|
+
self.artifact_store = artifact_store
|
|
103
|
+
self.content_limit = content_limit
|
|
104
|
+
self._catalogs: dict[str, McpCatalog] = {}
|
|
105
|
+
self._tool_catalogs = tool_catalogs if tool_catalogs is not None else {}
|
|
106
|
+
self._tool_catalog_locks: dict[str, asyncio.Lock] = {}
|
|
107
|
+
self._instructions_emitted: set[str] = set()
|
|
108
|
+
self._pending_context: list[SourcedContextMessage] = []
|
|
109
|
+
|
|
110
|
+
def drain_context(self) -> tuple[SourcedContextMessage, ...]:
|
|
111
|
+
messages = tuple(self._pending_context)
|
|
112
|
+
self._pending_context.clear()
|
|
113
|
+
return messages
|
|
114
|
+
|
|
115
|
+
def _emit_instructions(self, server_id: str, instructions: str | None) -> None:
|
|
116
|
+
if not instructions or server_id in self._instructions_emitted:
|
|
117
|
+
return
|
|
118
|
+
self._instructions_emitted.add(server_id)
|
|
119
|
+
self._pending_context.append(
|
|
120
|
+
SourcedContextMessage(f"mcp:{server_id}/instructions", instructions)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def catalog(self, server_id: str) -> McpCatalog:
|
|
124
|
+
cached = self._catalogs.get(server_id)
|
|
125
|
+
if cached is not None:
|
|
126
|
+
return cached
|
|
127
|
+
client = await self.runtime.activate(server_id)
|
|
128
|
+
initialize = client.initialize_result
|
|
129
|
+
if initialize is None:
|
|
130
|
+
raise RuntimeError("MCP Server did not initialize")
|
|
131
|
+
tool_values: list[Tool] = []
|
|
132
|
+
resource_values: list[Resource] = []
|
|
133
|
+
template_values: list[ResourceTemplate] = []
|
|
134
|
+
prompt_values: list[Prompt] = []
|
|
135
|
+
cursor: str | None = None
|
|
136
|
+
while True:
|
|
137
|
+
page = await client.list_tools(cursor)
|
|
138
|
+
tool_values.extend(page.tools)
|
|
139
|
+
cursor = page.nextCursor
|
|
140
|
+
if cursor is None:
|
|
141
|
+
break
|
|
142
|
+
cursor = None
|
|
143
|
+
while True:
|
|
144
|
+
resource_page = await client.list_resources(cursor)
|
|
145
|
+
resource_values.extend(resource_page.resources)
|
|
146
|
+
cursor = resource_page.nextCursor
|
|
147
|
+
if cursor is None:
|
|
148
|
+
break
|
|
149
|
+
cursor = None
|
|
150
|
+
while True:
|
|
151
|
+
template_page = await client.list_resource_templates(cursor)
|
|
152
|
+
template_values.extend(template_page.resourceTemplates)
|
|
153
|
+
cursor = template_page.nextCursor
|
|
154
|
+
if cursor is None:
|
|
155
|
+
break
|
|
156
|
+
cursor = None
|
|
157
|
+
while True:
|
|
158
|
+
prompt_page = await client.list_prompts(cursor)
|
|
159
|
+
prompt_values.extend(prompt_page.prompts)
|
|
160
|
+
cursor = prompt_page.nextCursor
|
|
161
|
+
if cursor is None:
|
|
162
|
+
break
|
|
163
|
+
catalog = build_catalog(
|
|
164
|
+
server_id,
|
|
165
|
+
initialize,
|
|
166
|
+
ListToolsResult(tools=tool_values),
|
|
167
|
+
ListResourcesResult(resources=resource_values),
|
|
168
|
+
ListPromptsResult(prompts=prompt_values),
|
|
169
|
+
ListResourceTemplatesResult(resourceTemplates=template_values),
|
|
170
|
+
)
|
|
171
|
+
self._catalogs[server_id] = catalog
|
|
172
|
+
self._tool_catalogs[server_id] = catalog.tools
|
|
173
|
+
self._emit_instructions(server_id, catalog.instructions)
|
|
174
|
+
return catalog
|
|
175
|
+
|
|
176
|
+
async def tool_catalog(self, server_id: str) -> tuple[McpToolDefinition, ...]:
|
|
177
|
+
"""List a server's tools without fetching resources/templates/prompts.
|
|
178
|
+
|
|
179
|
+
Tool discovery (search_mcp_tools) and direct registration only need
|
|
180
|
+
tools, so avoid the extra network round-trips a full catalog build makes.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
full = self._catalogs.get(server_id)
|
|
184
|
+
if full is not None:
|
|
185
|
+
return full.tools
|
|
186
|
+
cached = self._tool_catalogs.get(server_id)
|
|
187
|
+
if cached is not None:
|
|
188
|
+
return cached
|
|
189
|
+
lock = self._tool_catalog_locks.setdefault(server_id, asyncio.Lock())
|
|
190
|
+
async with lock:
|
|
191
|
+
cached = self._tool_catalogs.get(server_id)
|
|
192
|
+
if cached is not None:
|
|
193
|
+
return cached
|
|
194
|
+
client = await self.runtime.activate(server_id)
|
|
195
|
+
initialize = client.initialize_result
|
|
196
|
+
if initialize is None:
|
|
197
|
+
raise RuntimeError("MCP Server did not initialize")
|
|
198
|
+
tool_values: list[Tool] = []
|
|
199
|
+
cursor: str | None = None
|
|
200
|
+
while True:
|
|
201
|
+
page = await client.list_tools(cursor)
|
|
202
|
+
tool_values.extend(page.tools)
|
|
203
|
+
cursor = page.nextCursor
|
|
204
|
+
if cursor is None:
|
|
205
|
+
break
|
|
206
|
+
catalog = build_catalog(
|
|
207
|
+
server_id,
|
|
208
|
+
initialize,
|
|
209
|
+
ListToolsResult(tools=tool_values),
|
|
210
|
+
ListResourcesResult(resources=[]),
|
|
211
|
+
ListPromptsResult(prompts=[]),
|
|
212
|
+
)
|
|
213
|
+
self._tool_catalogs[server_id] = catalog.tools
|
|
214
|
+
self._emit_instructions(server_id, catalog.instructions)
|
|
215
|
+
return catalog.tools
|
|
216
|
+
|
|
217
|
+
async def search_tools(self, query: str = "") -> tuple[McpToolDefinition, ...]:
|
|
218
|
+
tool_lists = [await self.tool_catalog(server_id) for server_id in self.runtime.server_ids]
|
|
219
|
+
needle = query.casefold().strip()
|
|
220
|
+
return tuple(
|
|
221
|
+
tool
|
|
222
|
+
for tools in tool_lists
|
|
223
|
+
for tool in tools
|
|
224
|
+
if not needle or needle in tool.name.casefold() or needle in tool.description.casefold()
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def tool(self, stable_id: str) -> McpToolDefinition:
|
|
228
|
+
for definition in await self.search_tools():
|
|
229
|
+
if definition.stable_id == stable_id:
|
|
230
|
+
return definition
|
|
231
|
+
raise KeyError(f"unknown MCP tool: {stable_id}")
|
|
232
|
+
|
|
233
|
+
async def adapter(self, stable_id: str) -> McpToolAdapter:
|
|
234
|
+
definition = await self.tool(stable_id)
|
|
235
|
+
return McpToolAdapter(
|
|
236
|
+
definition,
|
|
237
|
+
self.runtime,
|
|
238
|
+
artifact_store=self.artifact_store,
|
|
239
|
+
output_limit=self.content_limit,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def register_selected_tools(
|
|
243
|
+
self, registry: ToolRegistry, stable_ids: set[str]
|
|
244
|
+
) -> tuple[str, ...]:
|
|
245
|
+
registered: list[str] = []
|
|
246
|
+
stale: list[str] = []
|
|
247
|
+
for stable_id in sorted(stable_ids):
|
|
248
|
+
try:
|
|
249
|
+
adapter = await self.adapter(stable_id)
|
|
250
|
+
except KeyError:
|
|
251
|
+
stale.append(stable_id)
|
|
252
|
+
continue
|
|
253
|
+
registry.register(adapter, replace=True)
|
|
254
|
+
registered.append(adapter.name)
|
|
255
|
+
stable_ids.difference_update(stale)
|
|
256
|
+
return tuple(registered)
|
|
257
|
+
|
|
258
|
+
async def register_direct_tools(
|
|
259
|
+
self, registry: ToolRegistry, *, direct_tool_limit: int
|
|
260
|
+
) -> tuple[str, ...]:
|
|
261
|
+
tool_lists = [
|
|
262
|
+
await self.tool_catalog(server_id) for server_id in self.runtime.required_server_ids
|
|
263
|
+
]
|
|
264
|
+
definitions = tuple(tool for tools in tool_lists for tool in tools)
|
|
265
|
+
if len(definitions) > direct_tool_limit:
|
|
266
|
+
return ()
|
|
267
|
+
registered: list[str] = []
|
|
268
|
+
for definition in sorted(definitions, key=lambda item: item.stable_id):
|
|
269
|
+
adapter = McpToolAdapter(
|
|
270
|
+
definition,
|
|
271
|
+
self.runtime,
|
|
272
|
+
artifact_store=self.artifact_store,
|
|
273
|
+
output_limit=self.content_limit,
|
|
274
|
+
)
|
|
275
|
+
registry.register(adapter, replace=True)
|
|
276
|
+
registered.append(adapter.name)
|
|
277
|
+
return tuple(registered)
|
|
278
|
+
|
|
279
|
+
async def resources(self, server_id: str) -> tuple[McpResourceDefinition, ...]:
|
|
280
|
+
return (await self.catalog(server_id)).resources
|
|
281
|
+
|
|
282
|
+
async def prompts(self, server_id: str) -> tuple[McpPromptDefinition, ...]:
|
|
283
|
+
return (await self.catalog(server_id)).prompts
|
|
284
|
+
|
|
285
|
+
async def read_resource(self, server_id: str, uri: str) -> SourcedMcpContent:
|
|
286
|
+
result = await (await self.runtime.activate(server_id)).read_resource(uri)
|
|
287
|
+
content = "\n".join(
|
|
288
|
+
item.text
|
|
289
|
+
if isinstance(item, TextResourceContents)
|
|
290
|
+
else json.dumps(item.model_dump(mode="json"), sort_keys=True)
|
|
291
|
+
for item in result.contents
|
|
292
|
+
)
|
|
293
|
+
return self._bounded(server_id, uri, content)
|
|
294
|
+
|
|
295
|
+
async def get_prompt(
|
|
296
|
+
self, server_id: str, name: str, arguments: dict[str, str] | None = None
|
|
297
|
+
) -> SourcedMcpContent:
|
|
298
|
+
result = await (await self.runtime.activate(server_id)).get_prompt(name, arguments)
|
|
299
|
+
content = "\n".join(
|
|
300
|
+
json.dumps(message.model_dump(mode="json"), sort_keys=True)
|
|
301
|
+
for message in result.messages
|
|
302
|
+
)
|
|
303
|
+
return self._bounded(server_id, name, content)
|
|
304
|
+
|
|
305
|
+
async def activate_prompt(self, name: str) -> SourcedMcpContent:
|
|
306
|
+
matches: list[tuple[str, McpPromptDefinition]] = []
|
|
307
|
+
for server_id in self.runtime.server_ids:
|
|
308
|
+
matches.extend(
|
|
309
|
+
(server_id, prompt)
|
|
310
|
+
for prompt in await self.prompts(server_id)
|
|
311
|
+
if prompt.name == name or prompt.stable_id == name
|
|
312
|
+
)
|
|
313
|
+
if not matches:
|
|
314
|
+
raise KeyError(f"unknown MCP prompt: {name}")
|
|
315
|
+
if len(matches) > 1:
|
|
316
|
+
raise ValueError(f"ambiguous MCP prompt: {name}")
|
|
317
|
+
server_id, prompt = matches[0]
|
|
318
|
+
content = await self.get_prompt(server_id, prompt.name)
|
|
319
|
+
self._pending_context.append(
|
|
320
|
+
SourcedContextMessage(f"mcp:{server_id}/prompt/{prompt.name}", content.content)
|
|
321
|
+
)
|
|
322
|
+
return content
|
|
323
|
+
|
|
324
|
+
async def instructions(self, server_id: str) -> SourcedMcpContent | None:
|
|
325
|
+
instructions = (await self.catalog(server_id)).instructions
|
|
326
|
+
return (
|
|
327
|
+
None if instructions is None else self._bounded(server_id, "instructions", instructions)
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def _bounded(self, server_id: str, identity: str, content: str) -> SourcedMcpContent:
|
|
331
|
+
if self.artifact_store is None:
|
|
332
|
+
return SourcedMcpContent(server_id, identity, content[: self.content_limit])
|
|
333
|
+
summary, reference = self.artifact_store.externalize(content, threshold=self.content_limit)
|
|
334
|
+
return SourcedMcpContent(
|
|
335
|
+
server_id,
|
|
336
|
+
identity,
|
|
337
|
+
summary,
|
|
338
|
+
None if reference is None else reference.relative_path,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class _StrictInput(BaseModel):
|
|
343
|
+
model_config = ConfigDict(extra="forbid")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class SearchMcpToolsInput(_StrictInput):
|
|
347
|
+
query: str = ""
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class ListMcpServersInput(_StrictInput):
|
|
351
|
+
include_disabled: bool = False
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class ListMcpServersTool:
|
|
355
|
+
name = "list_mcp_servers"
|
|
356
|
+
description = (
|
|
357
|
+
"List enabled MCP servers available to this configuration. Disabled servers are omitted "
|
|
358
|
+
"by default and must not be counted as current MCP servers. Set `include_disabled` to "
|
|
359
|
+
"true only when the user explicitly asks to inspect disabled MCP configuration."
|
|
360
|
+
)
|
|
361
|
+
input_model = ListMcpServersInput
|
|
362
|
+
effects = frozenset[ToolEffect]()
|
|
363
|
+
|
|
364
|
+
def __init__(
|
|
365
|
+
self,
|
|
366
|
+
records: tuple[CapabilityRecord, ...],
|
|
367
|
+
tool_catalogs: dict[str, tuple[McpToolDefinition, ...]],
|
|
368
|
+
selected_tools: set[str],
|
|
369
|
+
) -> None:
|
|
370
|
+
self.records = tuple(
|
|
371
|
+
record for record in records if record.kind is CapabilityKind.MCP_SERVER
|
|
372
|
+
)
|
|
373
|
+
self.tool_catalogs = tool_catalogs
|
|
374
|
+
self.selected_tools = selected_tools
|
|
375
|
+
|
|
376
|
+
async def execute(self, context: ToolContext, arguments: BaseModel) -> ToolResult:
|
|
377
|
+
del context
|
|
378
|
+
parsed = cast(ListMcpServersInput, arguments)
|
|
379
|
+
visible_records = tuple(
|
|
380
|
+
record for record in self.records if parsed.include_disabled or record.enabled
|
|
381
|
+
)
|
|
382
|
+
return ToolResult(
|
|
383
|
+
json.dumps(
|
|
384
|
+
{
|
|
385
|
+
"server_count": len(visible_records),
|
|
386
|
+
"includes_disabled": parsed.include_disabled,
|
|
387
|
+
"servers": [
|
|
388
|
+
{
|
|
389
|
+
"id": record.public_name,
|
|
390
|
+
"enabled": record.enabled,
|
|
391
|
+
"trusted": record.trusted,
|
|
392
|
+
"required": record.required,
|
|
393
|
+
"activation": record.activation.value,
|
|
394
|
+
"scope": record.source.scope.value,
|
|
395
|
+
"available_this_run": record.enabled and record.trusted,
|
|
396
|
+
"tool_catalog_cached": record.public_name in self.tool_catalogs,
|
|
397
|
+
"cached_tool_count": len(
|
|
398
|
+
self.tool_catalogs.get(record.public_name, ())
|
|
399
|
+
),
|
|
400
|
+
"selected_tool_count": sum(
|
|
401
|
+
tool_id.startswith(f"mcp:{record.public_name}/tool/")
|
|
402
|
+
for tool_id in self.selected_tools
|
|
403
|
+
),
|
|
404
|
+
}
|
|
405
|
+
for record in visible_records
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
sort_keys=True,
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class ReadMcpResourceInput(_StrictInput):
|
|
414
|
+
server_id: str = Field(min_length=1)
|
|
415
|
+
uri: str = Field(min_length=1)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class GetMcpPromptInput(_StrictInput):
|
|
419
|
+
server_id: str = Field(min_length=1)
|
|
420
|
+
name: str = Field(min_length=1)
|
|
421
|
+
arguments: dict[str, str] = Field(default_factory=dict[str, str])
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class SearchMcpToolsTool:
|
|
425
|
+
name = "search_mcp_tools"
|
|
426
|
+
description = (
|
|
427
|
+
"Discover and enable an MCP tool that is not already available. Search with a specific "
|
|
428
|
+
"keyword: one match is enabled immediately and returns its `call_name`; multiple matches "
|
|
429
|
+
"require one follow-up query using `select:<id>`. Call the returned tool directly and do "
|
|
430
|
+
"not repeat discovery. Enabled tools remain available in later runs."
|
|
431
|
+
)
|
|
432
|
+
input_model = SearchMcpToolsInput
|
|
433
|
+
effects = frozenset({ToolEffect.PROCESS, ToolEffect.NETWORK})
|
|
434
|
+
|
|
435
|
+
def __init__(
|
|
436
|
+
self,
|
|
437
|
+
service: McpCapabilityService,
|
|
438
|
+
registry: ToolRegistry,
|
|
439
|
+
selected_tools: set[str],
|
|
440
|
+
) -> None:
|
|
441
|
+
self.service = service
|
|
442
|
+
self.registry = registry
|
|
443
|
+
self.selected_tools = selected_tools
|
|
444
|
+
|
|
445
|
+
async def _select(self, stable_id: str) -> tuple[McpToolAdapter, bool]:
|
|
446
|
+
already_selected = stable_id in self.selected_tools
|
|
447
|
+
adapter = await self.service.adapter(stable_id)
|
|
448
|
+
self.registry.register(adapter, replace=True)
|
|
449
|
+
self.selected_tools.add(stable_id)
|
|
450
|
+
return adapter, already_selected
|
|
451
|
+
|
|
452
|
+
async def execute(self, context: ToolContext, arguments: BaseModel) -> ToolResult:
|
|
453
|
+
del context
|
|
454
|
+
parsed = cast(SearchMcpToolsInput, arguments)
|
|
455
|
+
if parsed.query.startswith("select:"):
|
|
456
|
+
stable_id = parsed.query.removeprefix("select:")
|
|
457
|
+
adapter, already_selected = await self._select(stable_id)
|
|
458
|
+
return ToolResult(
|
|
459
|
+
json.dumps(
|
|
460
|
+
{
|
|
461
|
+
"selected": stable_id,
|
|
462
|
+
"already_selected": already_selected,
|
|
463
|
+
"call_name": adapter.name,
|
|
464
|
+
"source": adapter.definition.server_id,
|
|
465
|
+
"next_step": (
|
|
466
|
+
f"call the tool named {adapter.name} directly with its arguments"
|
|
467
|
+
),
|
|
468
|
+
},
|
|
469
|
+
sort_keys=True,
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
tools = await self.service.search_tools(parsed.query)
|
|
473
|
+
if len(tools) == 1:
|
|
474
|
+
tool = tools[0]
|
|
475
|
+
adapter, already_selected = await self._select(tool.stable_id)
|
|
476
|
+
return ToolResult(
|
|
477
|
+
json.dumps(
|
|
478
|
+
{
|
|
479
|
+
"selected": tool.stable_id,
|
|
480
|
+
"already_selected": already_selected,
|
|
481
|
+
"call_name": adapter.name,
|
|
482
|
+
"source": tool.server_id,
|
|
483
|
+
"next_step": (
|
|
484
|
+
f"call the tool named {adapter.name} directly; do not call "
|
|
485
|
+
"search_mcp_tools again"
|
|
486
|
+
),
|
|
487
|
+
},
|
|
488
|
+
sort_keys=True,
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
return ToolResult(
|
|
492
|
+
json.dumps(
|
|
493
|
+
{
|
|
494
|
+
"tools": [
|
|
495
|
+
{
|
|
496
|
+
"id": tool.stable_id,
|
|
497
|
+
"name": tool.name,
|
|
498
|
+
"description": tool.description,
|
|
499
|
+
"source": tool.server_id,
|
|
500
|
+
}
|
|
501
|
+
for tool in tools
|
|
502
|
+
],
|
|
503
|
+
"hint": (
|
|
504
|
+
"Multiple tools matched. Enable the intended one with "
|
|
505
|
+
"query='select:<id>' exactly once, then call the returned call_name."
|
|
506
|
+
),
|
|
507
|
+
},
|
|
508
|
+
sort_keys=True,
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class ReadMcpResourceTool:
|
|
514
|
+
name = "read_mcp_resource"
|
|
515
|
+
description = "Read a resource from an activated MCP Server with source attribution."
|
|
516
|
+
input_model = ReadMcpResourceInput
|
|
517
|
+
effects = frozenset({ToolEffect.PROCESS, ToolEffect.NETWORK})
|
|
518
|
+
|
|
519
|
+
def __init__(self, service: McpCapabilityService) -> None:
|
|
520
|
+
self.service = service
|
|
521
|
+
|
|
522
|
+
async def execute(self, context: ToolContext, arguments: BaseModel) -> ToolResult:
|
|
523
|
+
del context
|
|
524
|
+
parsed = cast(ReadMcpResourceInput, arguments)
|
|
525
|
+
content = await self.service.read_resource(parsed.server_id, parsed.uri)
|
|
526
|
+
return ToolResult(
|
|
527
|
+
content.content,
|
|
528
|
+
artifact_ref=content.artifact_ref,
|
|
529
|
+
data={"source": content.server_id, "identity": content.identity},
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class GetMcpPromptTool:
|
|
534
|
+
name = "get_mcp_prompt"
|
|
535
|
+
description = "Get a prompt from an activated MCP Server with source attribution."
|
|
536
|
+
input_model = GetMcpPromptInput
|
|
537
|
+
effects = frozenset({ToolEffect.PROCESS, ToolEffect.NETWORK})
|
|
538
|
+
|
|
539
|
+
def __init__(self, service: McpCapabilityService) -> None:
|
|
540
|
+
self.service = service
|
|
541
|
+
|
|
542
|
+
async def execute(self, context: ToolContext, arguments: BaseModel) -> ToolResult:
|
|
543
|
+
del context
|
|
544
|
+
parsed = cast(GetMcpPromptInput, arguments)
|
|
545
|
+
content = await self.service.get_prompt(parsed.server_id, parsed.name, parsed.arguments)
|
|
546
|
+
return ToolResult(
|
|
547
|
+
content.content,
|
|
548
|
+
artifact_ref=content.artifact_ref,
|
|
549
|
+
data={"source": content.server_id, "identity": content.identity},
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def register_mcp_management_tools(
|
|
554
|
+
registry: ToolRegistry, service: McpCapabilityService, selected_tools: set[str]
|
|
555
|
+
) -> None:
|
|
556
|
+
for tool in (
|
|
557
|
+
SearchMcpToolsTool(service, registry, selected_tools),
|
|
558
|
+
ReadMcpResourceTool(service),
|
|
559
|
+
GetMcpPromptTool(service),
|
|
560
|
+
):
|
|
561
|
+
registry.register(tool)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def register_mcp_status_tool(
|
|
565
|
+
registry: ToolRegistry,
|
|
566
|
+
records: tuple[CapabilityRecord, ...],
|
|
567
|
+
tool_catalogs: dict[str, tuple[McpToolDefinition, ...]],
|
|
568
|
+
selected_tools: set[str],
|
|
569
|
+
) -> None:
|
|
570
|
+
registry.register(ListMcpServersTool(records, tool_catalogs, selected_tools))
|