abstractcore 2.6.9__py3-none-any.whl → 2.9.1__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.
- abstractcore/apps/summarizer.py +69 -27
- abstractcore/architectures/detection.py +190 -25
- abstractcore/assets/architecture_formats.json +129 -6
- abstractcore/assets/model_capabilities.json +803 -141
- abstractcore/config/main.py +2 -2
- abstractcore/config/manager.py +3 -1
- abstractcore/events/__init__.py +7 -1
- abstractcore/mcp/__init__.py +30 -0
- abstractcore/mcp/client.py +213 -0
- abstractcore/mcp/factory.py +64 -0
- abstractcore/mcp/naming.py +28 -0
- abstractcore/mcp/stdio_client.py +336 -0
- abstractcore/mcp/tool_source.py +164 -0
- abstractcore/processing/__init__.py +2 -2
- abstractcore/processing/basic_deepsearch.py +1 -1
- abstractcore/processing/basic_summarizer.py +379 -93
- abstractcore/providers/anthropic_provider.py +91 -10
- abstractcore/providers/base.py +540 -16
- abstractcore/providers/huggingface_provider.py +17 -8
- abstractcore/providers/lmstudio_provider.py +170 -25
- abstractcore/providers/mlx_provider.py +13 -10
- abstractcore/providers/ollama_provider.py +42 -26
- abstractcore/providers/openai_compatible_provider.py +87 -22
- abstractcore/providers/openai_provider.py +12 -9
- abstractcore/providers/streaming.py +201 -39
- abstractcore/providers/vllm_provider.py +78 -21
- abstractcore/server/app.py +116 -30
- abstractcore/structured/retry.py +20 -7
- abstractcore/tools/__init__.py +46 -24
- abstractcore/tools/abstractignore.py +166 -0
- abstractcore/tools/arg_canonicalizer.py +61 -0
- abstractcore/tools/common_tools.py +2443 -742
- abstractcore/tools/core.py +109 -13
- abstractcore/tools/handler.py +17 -3
- abstractcore/tools/parser.py +894 -159
- abstractcore/tools/registry.py +122 -18
- abstractcore/tools/syntax_rewriter.py +68 -6
- abstractcore/tools/tag_rewriter.py +186 -1
- abstractcore/utils/jsonish.py +111 -0
- abstractcore/utils/version.py +1 -1
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/METADATA +56 -2
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/RECORD +46 -37
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/WHEEL +0 -0
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/entry_points.txt +0 -0
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/licenses/LICENSE +0 -0
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from collections import deque
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from importlib import metadata
|
|
12
|
+
from typing import Any, Deque, Dict, List, Optional, Sequence
|
|
13
|
+
|
|
14
|
+
from .client import McpError, McpJsonRpcRequest, McpProtocolError, McpRpcError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_DEFAULT_PROTOCOL_VERSION = "2025-11-25"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class McpStdioServerParameters:
|
|
22
|
+
command: List[str]
|
|
23
|
+
cwd: Optional[str] = None
|
|
24
|
+
env: Optional[Dict[str, str]] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class McpStdioClient:
|
|
28
|
+
"""A minimal MCP JSON-RPC client using stdio (spawn a subprocess).
|
|
29
|
+
|
|
30
|
+
This client is synchronous and intentionally small: it focuses on the tools surface
|
|
31
|
+
(`tools/list`, `tools/call`) and includes a best-effort MCP initialization handshake.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
command: Sequence[str],
|
|
38
|
+
cwd: Optional[str] = None,
|
|
39
|
+
env: Optional[Dict[str, str]] = None,
|
|
40
|
+
timeout_s: Optional[float] = 30.0,
|
|
41
|
+
protocol_version: Optional[str] = None,
|
|
42
|
+
client_name: str = "abstractcore.mcp",
|
|
43
|
+
client_version: Optional[str] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
cmd = [str(c) for c in (command or []) if str(c).strip()]
|
|
46
|
+
if not cmd:
|
|
47
|
+
raise ValueError("McpStdioClient requires a non-empty command")
|
|
48
|
+
|
|
49
|
+
self._timeout_s = float(timeout_s) if timeout_s is not None else None
|
|
50
|
+
self._id_iter = itertools.count(1)
|
|
51
|
+
self._init_attempted = False
|
|
52
|
+
self._initialized = False
|
|
53
|
+
|
|
54
|
+
self._proc = subprocess.Popen(
|
|
55
|
+
cmd,
|
|
56
|
+
stdin=subprocess.PIPE,
|
|
57
|
+
stdout=subprocess.PIPE,
|
|
58
|
+
stderr=subprocess.PIPE,
|
|
59
|
+
text=True,
|
|
60
|
+
encoding="utf-8",
|
|
61
|
+
bufsize=1,
|
|
62
|
+
cwd=str(cwd) if cwd else None,
|
|
63
|
+
env=self._merge_env(env),
|
|
64
|
+
)
|
|
65
|
+
if self._proc.stdin is None or self._proc.stdout is None:
|
|
66
|
+
raise McpError("Failed to start MCP stdio subprocess with pipes")
|
|
67
|
+
|
|
68
|
+
self._lock = threading.Lock()
|
|
69
|
+
self._cond = threading.Condition(self._lock)
|
|
70
|
+
self._responses: Dict[int, Dict[str, Any]] = {}
|
|
71
|
+
self._global_error: Optional[Dict[str, Any]] = None
|
|
72
|
+
self._closed = False
|
|
73
|
+
self._stderr_tail: Deque[str] = deque(maxlen=200)
|
|
74
|
+
|
|
75
|
+
self._protocol_version = str(protocol_version).strip() if protocol_version else _DEFAULT_PROTOCOL_VERSION
|
|
76
|
+
self._client_name = str(client_name or "abstractcore.mcp").strip() or "abstractcore.mcp"
|
|
77
|
+
self._client_version = str(client_version).strip() if client_version else self._default_client_version()
|
|
78
|
+
|
|
79
|
+
self._stdout_thread = threading.Thread(target=self._read_stdout_loop, daemon=True)
|
|
80
|
+
self._stderr_thread = threading.Thread(target=self._read_stderr_loop, daemon=True)
|
|
81
|
+
self._stdout_thread.start()
|
|
82
|
+
self._stderr_thread.start()
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _default_client_version() -> str:
|
|
86
|
+
for pkg in ("abstractcore", "AbstractCore"):
|
|
87
|
+
try:
|
|
88
|
+
v = str(metadata.version(pkg) or "").strip()
|
|
89
|
+
except Exception:
|
|
90
|
+
v = ""
|
|
91
|
+
if v:
|
|
92
|
+
return v
|
|
93
|
+
return "0.0.0"
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _merge_env(env: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
|
|
97
|
+
if env is None:
|
|
98
|
+
return None
|
|
99
|
+
merged = dict(os.environ)
|
|
100
|
+
for k, v in env.items():
|
|
101
|
+
if not isinstance(k, str) or not k.strip():
|
|
102
|
+
continue
|
|
103
|
+
if v is None:
|
|
104
|
+
continue
|
|
105
|
+
merged[str(k)] = str(v)
|
|
106
|
+
return merged
|
|
107
|
+
|
|
108
|
+
def close(self) -> None:
|
|
109
|
+
with self._cond:
|
|
110
|
+
if self._closed:
|
|
111
|
+
return
|
|
112
|
+
self._closed = True
|
|
113
|
+
self._cond.notify_all()
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
if self._proc.stdin:
|
|
117
|
+
try:
|
|
118
|
+
self._proc.stdin.close()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
if self._proc.stdout:
|
|
122
|
+
try:
|
|
123
|
+
self._proc.stdout.close()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
finally:
|
|
127
|
+
try:
|
|
128
|
+
self._proc.terminate()
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
try:
|
|
132
|
+
self._proc.wait(timeout=2)
|
|
133
|
+
except Exception:
|
|
134
|
+
try:
|
|
135
|
+
self._proc.kill()
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
def __enter__(self) -> "McpStdioClient":
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
143
|
+
self.close()
|
|
144
|
+
|
|
145
|
+
def _read_stdout_loop(self) -> None:
|
|
146
|
+
try:
|
|
147
|
+
assert self._proc.stdout is not None
|
|
148
|
+
for line in self._proc.stdout:
|
|
149
|
+
text = (line or "").strip()
|
|
150
|
+
if not text:
|
|
151
|
+
continue
|
|
152
|
+
try:
|
|
153
|
+
msg = json.loads(text)
|
|
154
|
+
except Exception:
|
|
155
|
+
continue
|
|
156
|
+
if not isinstance(msg, dict):
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
msg_id = msg.get("id")
|
|
160
|
+
if msg_id is None:
|
|
161
|
+
# JSON-RPC error may legally have id=null; surface as a global error.
|
|
162
|
+
if msg.get("error") is not None:
|
|
163
|
+
with self._cond:
|
|
164
|
+
self._global_error = dict(msg)
|
|
165
|
+
self._cond.notify_all()
|
|
166
|
+
continue
|
|
167
|
+
try:
|
|
168
|
+
mid = int(msg_id)
|
|
169
|
+
except Exception:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
with self._cond:
|
|
173
|
+
self._responses[mid] = dict(msg)
|
|
174
|
+
self._cond.notify_all()
|
|
175
|
+
finally:
|
|
176
|
+
with self._cond:
|
|
177
|
+
self._closed = True
|
|
178
|
+
self._cond.notify_all()
|
|
179
|
+
|
|
180
|
+
def _read_stderr_loop(self) -> None:
|
|
181
|
+
try:
|
|
182
|
+
if self._proc.stderr is None:
|
|
183
|
+
return
|
|
184
|
+
for line in self._proc.stderr:
|
|
185
|
+
text = (line or "").rstrip()
|
|
186
|
+
if not text:
|
|
187
|
+
continue
|
|
188
|
+
with self._cond:
|
|
189
|
+
self._stderr_tail.append(text)
|
|
190
|
+
except Exception:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
def _send(self, payload: Dict[str, Any]) -> None:
|
|
194
|
+
if self._proc.stdin is None:
|
|
195
|
+
raise McpError("MCP stdio process stdin is closed")
|
|
196
|
+
try:
|
|
197
|
+
self._proc.stdin.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
198
|
+
self._proc.stdin.flush()
|
|
199
|
+
except Exception as e:
|
|
200
|
+
raise McpError(f"Failed to write to MCP stdio process: {e}") from e
|
|
201
|
+
|
|
202
|
+
def _wait_for(self, req_id: int) -> Dict[str, Any]:
|
|
203
|
+
deadline: Optional[float]
|
|
204
|
+
if self._timeout_s is None:
|
|
205
|
+
deadline = None
|
|
206
|
+
else:
|
|
207
|
+
deadline = time.time() + self._timeout_s
|
|
208
|
+
|
|
209
|
+
with self._cond:
|
|
210
|
+
while True:
|
|
211
|
+
if req_id in self._responses:
|
|
212
|
+
return self._responses.pop(req_id)
|
|
213
|
+
if self._global_error is not None:
|
|
214
|
+
err = self._global_error
|
|
215
|
+
self._global_error = None
|
|
216
|
+
raise McpProtocolError(f"MCP stdio global error: {err}")
|
|
217
|
+
if self._closed:
|
|
218
|
+
tail = "\n".join(list(self._stderr_tail)[-20:])
|
|
219
|
+
raise McpError(f"MCP stdio process closed unexpectedly.\n\nstderr tail:\n{tail}")
|
|
220
|
+
|
|
221
|
+
if deadline is None:
|
|
222
|
+
self._cond.wait(timeout=0.25)
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
remaining = deadline - time.time()
|
|
226
|
+
if remaining <= 0:
|
|
227
|
+
tail = "\n".join(list(self._stderr_tail)[-20:])
|
|
228
|
+
raise McpError(f"MCP stdio request timed out after {self._timeout_s}s.\n\nstderr tail:\n{tail}")
|
|
229
|
+
self._cond.wait(timeout=min(0.25, remaining))
|
|
230
|
+
|
|
231
|
+
def _request_no_init(self, *, method: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
232
|
+
mid = str(method or "").strip()
|
|
233
|
+
if not mid:
|
|
234
|
+
raise ValueError("MCP request requires a non-empty method")
|
|
235
|
+
|
|
236
|
+
req = McpJsonRpcRequest(jsonrpc="2.0", id=next(self._id_iter), method=mid, params=params)
|
|
237
|
+
self._send(req.to_dict())
|
|
238
|
+
resp = self._wait_for(req.id)
|
|
239
|
+
|
|
240
|
+
if resp.get("jsonrpc") != "2.0":
|
|
241
|
+
raise McpProtocolError("MCP response missing jsonrpc='2.0'")
|
|
242
|
+
|
|
243
|
+
if "error" in resp and resp["error"] is not None:
|
|
244
|
+
err = resp["error"]
|
|
245
|
+
if isinstance(err, dict):
|
|
246
|
+
raise McpRpcError(
|
|
247
|
+
code=int(err.get("code") or 0),
|
|
248
|
+
message=str(err.get("message") or "Unknown error"),
|
|
249
|
+
data=err.get("data"),
|
|
250
|
+
)
|
|
251
|
+
raise McpRpcError(code=-32000, message=str(err), data=None)
|
|
252
|
+
|
|
253
|
+
resp_id = resp.get("id")
|
|
254
|
+
if resp_id is None:
|
|
255
|
+
raise McpProtocolError("MCP response missing id")
|
|
256
|
+
if str(resp_id) != str(req.id):
|
|
257
|
+
raise McpProtocolError(f"MCP response id mismatch (expected {req.id}, got {resp_id})")
|
|
258
|
+
|
|
259
|
+
result = resp.get("result")
|
|
260
|
+
if not isinstance(result, dict):
|
|
261
|
+
raise McpProtocolError("MCP response missing result object")
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
def _ensure_initialized(self) -> None:
|
|
265
|
+
if self._initialized or self._init_attempted:
|
|
266
|
+
return
|
|
267
|
+
self._init_attempted = True
|
|
268
|
+
|
|
269
|
+
params: Dict[str, Any] = {
|
|
270
|
+
"protocolVersion": self._protocol_version,
|
|
271
|
+
# Match the MCP reference client's envelope shape (server-side validators often
|
|
272
|
+
# expect these keys to exist even when the values are null).
|
|
273
|
+
"capabilities": {
|
|
274
|
+
"experimental": None,
|
|
275
|
+
"sampling": None,
|
|
276
|
+
"elicitation": None,
|
|
277
|
+
"roots": None,
|
|
278
|
+
"tasks": None,
|
|
279
|
+
},
|
|
280
|
+
"clientInfo": {"name": self._client_name, "version": self._client_version or "0.0.0"},
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
self._request_no_init(method="initialize", params=params)
|
|
285
|
+
# Best-effort "initialized" notification.
|
|
286
|
+
try:
|
|
287
|
+
self.notify(method="initialized", params={})
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
self._initialized = True
|
|
291
|
+
except McpRpcError as e:
|
|
292
|
+
# Some non-conformant servers may not implement initialize; allow continuing.
|
|
293
|
+
if int(getattr(e, "code", 0)) == -32601:
|
|
294
|
+
self._initialized = True
|
|
295
|
+
return
|
|
296
|
+
raise
|
|
297
|
+
|
|
298
|
+
def notify(self, *, method: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
299
|
+
mid = str(method or "").strip()
|
|
300
|
+
if not mid:
|
|
301
|
+
raise ValueError("MCP notify requires a non-empty method")
|
|
302
|
+
payload: Dict[str, Any] = {"jsonrpc": "2.0", "method": mid}
|
|
303
|
+
if params is not None:
|
|
304
|
+
payload["params"] = params
|
|
305
|
+
self._send(payload)
|
|
306
|
+
|
|
307
|
+
def request(self, *, method: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
308
|
+
if str(method or "").strip() != "initialize":
|
|
309
|
+
self._ensure_initialized()
|
|
310
|
+
return self._request_no_init(method=method, params=params)
|
|
311
|
+
|
|
312
|
+
def list_tools(self, *, cursor: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
313
|
+
params: Optional[Dict[str, Any]] = None
|
|
314
|
+
if cursor is not None:
|
|
315
|
+
params = {"cursor": str(cursor)}
|
|
316
|
+
|
|
317
|
+
result = self.request(method="tools/list", params=params)
|
|
318
|
+
tools = result.get("tools")
|
|
319
|
+
if not isinstance(tools, list):
|
|
320
|
+
raise McpProtocolError("MCP tools/list result missing tools list")
|
|
321
|
+
out: List[Dict[str, Any]] = []
|
|
322
|
+
for t in tools:
|
|
323
|
+
if isinstance(t, dict):
|
|
324
|
+
out.append(t)
|
|
325
|
+
return out
|
|
326
|
+
|
|
327
|
+
def call_tool(self, *, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
328
|
+
tool_name = str(name or "").strip()
|
|
329
|
+
if not tool_name:
|
|
330
|
+
raise ValueError("MCP tools/call requires a non-empty tool name")
|
|
331
|
+
args = dict(arguments or {})
|
|
332
|
+
|
|
333
|
+
result = self.request(method="tools/call", params={"name": tool_name, "arguments": args})
|
|
334
|
+
if not isinstance(result, dict):
|
|
335
|
+
raise McpProtocolError("MCP tools/call result must be an object")
|
|
336
|
+
return result
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, List, Optional, Protocol, Set
|
|
5
|
+
|
|
6
|
+
from .naming import namespaced_tool_name
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _first_non_empty_line(text: Optional[str]) -> str:
|
|
10
|
+
if not text:
|
|
11
|
+
return ""
|
|
12
|
+
for line in str(text).splitlines():
|
|
13
|
+
s = line.strip()
|
|
14
|
+
if s:
|
|
15
|
+
return s
|
|
16
|
+
return ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_one_line(text: Optional[str]) -> str:
|
|
20
|
+
return " ".join(str(text or "").split()).strip()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _short_description(text: Optional[str], *, max_chars: int = 200) -> str:
|
|
24
|
+
one = _normalize_one_line(_first_non_empty_line(text))
|
|
25
|
+
if not one:
|
|
26
|
+
return ""
|
|
27
|
+
if len(one) <= max_chars:
|
|
28
|
+
return one
|
|
29
|
+
if max_chars <= 3:
|
|
30
|
+
return one[:max_chars]
|
|
31
|
+
return one[: max_chars - 3].rstrip() + "..."
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class McpServerInfo:
|
|
36
|
+
server_id: str
|
|
37
|
+
url: str
|
|
38
|
+
transport: str = "streamable_http"
|
|
39
|
+
version: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class McpToolClient(Protocol):
|
|
43
|
+
def list_tools(self, *, cursor: Optional[str] = None) -> List[Dict[str, Any]]: ...
|
|
44
|
+
|
|
45
|
+
def call_tool(self, *, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: ...
|
|
46
|
+
|
|
47
|
+
def close(self) -> None: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _normalize_mcp_parameters(input_schema: Any) -> Dict[str, Any]:
|
|
51
|
+
"""Convert an MCP inputSchema into AbstractCore's compact parameter map.
|
|
52
|
+
|
|
53
|
+
AbstractCore's tool prompts and native tool formatting treat a parameter as required
|
|
54
|
+
when its schema dict omits a `default`. Therefore, for MCP schemas:
|
|
55
|
+
- required properties => ensure `default` is absent
|
|
56
|
+
- optional properties => set `default` to None (unless a default is already provided)
|
|
57
|
+
"""
|
|
58
|
+
if not isinstance(input_schema, dict):
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
props = input_schema.get("properties")
|
|
62
|
+
if not isinstance(props, dict):
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
required_raw = input_schema.get("required")
|
|
66
|
+
required: Set[str] = set()
|
|
67
|
+
if isinstance(required_raw, list):
|
|
68
|
+
required = {str(x) for x in required_raw if isinstance(x, str) and x.strip()}
|
|
69
|
+
|
|
70
|
+
out: Dict[str, Any] = {}
|
|
71
|
+
for key, schema in props.items():
|
|
72
|
+
if not isinstance(key, str) or not key.strip():
|
|
73
|
+
continue
|
|
74
|
+
meta: Dict[str, Any]
|
|
75
|
+
if isinstance(schema, dict):
|
|
76
|
+
meta = dict(schema)
|
|
77
|
+
else:
|
|
78
|
+
meta = {}
|
|
79
|
+
|
|
80
|
+
if key in required:
|
|
81
|
+
meta.pop("default", None)
|
|
82
|
+
else:
|
|
83
|
+
meta.setdefault("default", None)
|
|
84
|
+
|
|
85
|
+
out[key] = meta
|
|
86
|
+
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def mcp_tool_to_abstractcore_tool_spec(
|
|
91
|
+
tool: Dict[str, Any],
|
|
92
|
+
*,
|
|
93
|
+
server: McpServerInfo,
|
|
94
|
+
) -> Dict[str, Any]:
|
|
95
|
+
"""Convert a single MCP tool entry into an AbstractCore tool spec dict."""
|
|
96
|
+
raw_name = tool.get("name")
|
|
97
|
+
name = str(raw_name or "").strip()
|
|
98
|
+
if not name:
|
|
99
|
+
raise ValueError("MCP tool is missing a valid name")
|
|
100
|
+
|
|
101
|
+
raw_description = tool.get("description") or tool.get("title") or ""
|
|
102
|
+
description = _short_description(str(raw_description or ""), max_chars=200)
|
|
103
|
+
if not description:
|
|
104
|
+
description = _short_description(f"MCP tool '{name}'", max_chars=200) or f"MCP tool '{name}'"
|
|
105
|
+
|
|
106
|
+
parameters = _normalize_mcp_parameters(tool.get("inputSchema"))
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"name": namespaced_tool_name(server_id=server.server_id, tool_name=name),
|
|
110
|
+
"description": description,
|
|
111
|
+
"parameters": parameters,
|
|
112
|
+
"tags": ["mcp", f"mcp_server:{server.server_id}"],
|
|
113
|
+
"origin": {
|
|
114
|
+
"type": "mcp",
|
|
115
|
+
"server_id": server.server_id,
|
|
116
|
+
"tool_name": name,
|
|
117
|
+
"transport": server.transport,
|
|
118
|
+
"url": server.url,
|
|
119
|
+
"version": server.version,
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class McpToolSource:
|
|
125
|
+
"""Discover tools from an MCP server and return AbstractCore-compatible tool specs."""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
*,
|
|
130
|
+
server_id: str,
|
|
131
|
+
client: McpToolClient,
|
|
132
|
+
transport: str = "streamable_http",
|
|
133
|
+
origin_url: Optional[str] = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
sid = str(server_id or "").strip()
|
|
136
|
+
if not sid:
|
|
137
|
+
raise ValueError("McpToolSource requires a non-empty server_id")
|
|
138
|
+
transport_norm = str(transport or "").strip() or "streamable_http"
|
|
139
|
+
|
|
140
|
+
url = str(origin_url or "").strip() if origin_url else ""
|
|
141
|
+
if not url:
|
|
142
|
+
candidate = getattr(client, "url", None)
|
|
143
|
+
if isinstance(candidate, str) and candidate.strip():
|
|
144
|
+
url = candidate.strip()
|
|
145
|
+
if not url:
|
|
146
|
+
url = f"{transport_norm}://{sid}"
|
|
147
|
+
|
|
148
|
+
self._server = McpServerInfo(server_id=sid, url=url, transport=transport_norm)
|
|
149
|
+
self._client = client
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def server(self) -> McpServerInfo:
|
|
153
|
+
return self._server
|
|
154
|
+
|
|
155
|
+
def list_tool_specs(self) -> List[Dict[str, Any]]:
|
|
156
|
+
tools = self._client.list_tools()
|
|
157
|
+
specs: List[Dict[str, Any]] = []
|
|
158
|
+
for t in tools:
|
|
159
|
+
try:
|
|
160
|
+
specs.append(mcp_tool_to_abstractcore_tool_spec(t, server=self._server))
|
|
161
|
+
except Exception:
|
|
162
|
+
# Skip invalid tool entries; listing should be best-effort.
|
|
163
|
+
continue
|
|
164
|
+
return specs
|
|
@@ -5,14 +5,14 @@ Basic text processing capabilities built on top of AbstractCore,
|
|
|
5
5
|
demonstrating how to leverage the core infrastructure for real-world tasks.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .basic_summarizer import BasicSummarizer, SummaryStyle, SummaryLength
|
|
8
|
+
from .basic_summarizer import BasicSummarizer, SummaryStyle, SummaryLength, CompressionMode
|
|
9
9
|
from .basic_extractor import BasicExtractor
|
|
10
10
|
from .basic_judge import BasicJudge, JudgmentCriteria, Assessment, create_judge
|
|
11
11
|
from .basic_deepsearch import BasicDeepSearch, ResearchReport, ResearchFinding, ResearchPlan, ResearchSubTask
|
|
12
12
|
from .basic_intent import BasicIntentAnalyzer, IntentType, IntentDepth, IntentContext, IdentifiedIntent, IntentAnalysisOutput
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
|
-
'BasicSummarizer', 'SummaryStyle', 'SummaryLength',
|
|
15
|
+
'BasicSummarizer', 'SummaryStyle', 'SummaryLength', 'CompressionMode',
|
|
16
16
|
'BasicExtractor',
|
|
17
17
|
'BasicJudge', 'JudgmentCriteria', 'Assessment', 'create_judge',
|
|
18
18
|
'BasicDeepSearch', 'ResearchReport', 'ResearchFinding', 'ResearchPlan', 'ResearchSubTask',
|
|
@@ -757,7 +757,7 @@ Avoid generic terms like "qubit" alone (which returns lab instruments) - be spec
|
|
|
757
757
|
|
|
758
758
|
try:
|
|
759
759
|
logger.debug(f"🌐 Fetching content from URL {i+1}: {url}")
|
|
760
|
-
content = fetch_url(url, timeout=15)
|
|
760
|
+
content = fetch_url(url, timeout=15, include_full_content=self.full_text_extraction)
|
|
761
761
|
|
|
762
762
|
if "Error" in content or len(content) < 100:
|
|
763
763
|
logger.debug(f"⚠️ Skipping URL due to fetch error or short content: {url}")
|