fleetmesh-node 0.2.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 (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,454 @@
1
+ """Model Context Protocol (MCP) Client Bridge & Tool Adapter for FleetMesh Compute.
2
+
3
+ Provides:
4
+ - `McpToolSpec`: Schema descriptor for an MCP tool (`name`, `description`, `inputSchema`).
5
+ - `McpResourceSpec`: Schema descriptor for an MCP resource (`uri`, `name`, `description`, `mimeType`).
6
+ - `McpServerConfig`: Connection & capability configuration for an MCP server (`builtin_mesh`,
7
+ `http_jsonrpc`, or `http_sse`).
8
+ - `McpToolAdapter`: Adapts any MCP tool into FleetMesh's `AgentToolProtocol` so `AgentExecutor`
9
+ can invoke MCP tools inside the ReAct reasoning loop with fuel accounting.
10
+ - `McpClientBridge`: Executes JSON-RPC 2.0 `tools/list`, `tools/call`, `resources/list`, and
11
+ `resources/read` against remote HTTP MCP endpoints or in-process FleetMesh/MeshTube handlers.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import time
17
+ import uuid
18
+ from dataclasses import dataclass, field
19
+ from typing import Any, Callable, Mapping, Optional, Sequence
20
+
21
+ from node.transport import netfetch
22
+ from .agent import AgentExecutionContext, AgentToolResult
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class McpToolSpec:
27
+ """Descriptor for a tool exposed by an MCP server."""
28
+
29
+ name: str
30
+ description: str
31
+ input_schema: Mapping[str, Any] = field(default_factory=lambda: {'type': 'object', 'properties': {}})
32
+ server_id: str = ''
33
+
34
+ def to_dict(self) -> dict[str, Any]:
35
+ return {
36
+ 'name': self.name,
37
+ 'description': self.description,
38
+ 'inputSchema': dict(self.input_schema),
39
+ 'server_id': self.server_id,
40
+ }
41
+
42
+ @classmethod
43
+ def from_mapping(cls, data: Mapping[str, Any], server_id: str = '') -> McpToolSpec:
44
+ schema = data.get('inputSchema') or data.get('input_schema') or data.get('parameters') or {
45
+ 'type': 'object',
46
+ 'properties': {},
47
+ }
48
+ return cls(
49
+ name=str(data.get('name') or '').strip(),
50
+ description=str(data.get('description') or '').strip(),
51
+ input_schema=dict(schema) if isinstance(schema, Mapping) else {'type': 'object', 'properties': {}},
52
+ server_id=str(data.get('server_id') or server_id).strip(),
53
+ )
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class McpResourceSpec:
58
+ """Descriptor for a context resource exposed by an MCP server."""
59
+
60
+ uri: str
61
+ name: str
62
+ description: str = ''
63
+ mime_type: str = 'application/json'
64
+ server_id: str = ''
65
+
66
+ def to_dict(self) -> dict[str, Any]:
67
+ return {
68
+ 'uri': self.uri,
69
+ 'name': self.name,
70
+ 'description': self.description,
71
+ 'mimeType': self.mime_type,
72
+ 'server_id': self.server_id,
73
+ }
74
+
75
+ @classmethod
76
+ def from_mapping(cls, data: Mapping[str, Any], server_id: str = '') -> McpResourceSpec:
77
+ return cls(
78
+ uri=str(data.get('uri') or '').strip(),
79
+ name=str(data.get('name') or '').strip(),
80
+ description=str(data.get('description') or '').strip(),
81
+ mime_type=str(data.get('mimeType') or data.get('mime_type') or 'application/json').strip(),
82
+ server_id=str(data.get('server_id') or server_id).strip(),
83
+ )
84
+
85
+
86
+ @dataclass
87
+ class McpServerConfig:
88
+ """Configuration and discovered capabilities for an MCP server."""
89
+
90
+ server_id: str
91
+ name: str
92
+ transport: str = 'http_jsonrpc' # 'builtin_mesh' | 'http_jsonrpc' | 'http_sse'
93
+ endpoint: str = ''
94
+ description: str = ''
95
+ auth_header: str = 'Authorization'
96
+ auth_token: str = ''
97
+ enabled: bool = True
98
+ tools: tuple[McpToolSpec, ...] = ()
99
+ resources: tuple[McpResourceSpec, ...] = ()
100
+ status: str = 'connected'
101
+ last_synced_at: int = 0
102
+
103
+ def build_headers(self) -> dict[str, str]:
104
+ headers: dict[str, str] = {
105
+ 'Content-Type': 'application/json',
106
+ 'Accept': 'application/json, text/event-stream',
107
+ }
108
+ tok = self.auth_token.strip()
109
+ if tok:
110
+ hdr = self.auth_header.strip() or 'Authorization'
111
+ if hdr.lower() == 'authorization' and not tok.lower().startswith(('bearer ', 'basic ', 'cashu')):
112
+ headers[hdr] = f'Bearer {tok}'
113
+ else:
114
+ headers[hdr] = tok
115
+ return headers
116
+
117
+ def to_dict(self, *, redact_secrets: bool = True) -> dict[str, Any]:
118
+ tok = self.auth_token.strip()
119
+ masked_tok = f'{tok[:6]}...{tok[-4:]}' if len(tok) > 10 else ('***' if tok else '')
120
+ return {
121
+ 'server_id': self.server_id,
122
+ 'name': self.name,
123
+ 'transport': self.transport,
124
+ 'endpoint': self.endpoint,
125
+ 'description': self.description,
126
+ 'auth_header': self.auth_header,
127
+ 'has_auth_token': bool(tok),
128
+ 'auth_token_preview': masked_tok if redact_secrets else tok,
129
+ 'enabled': self.enabled,
130
+ 'tools': [t.to_dict() for t in self.tools],
131
+ 'resources': [r.to_dict() for r in self.resources],
132
+ 'status': self.status,
133
+ 'last_synced_at': self.last_synced_at,
134
+ }
135
+
136
+
137
+ class McpToolAdapter:
138
+ """Wraps an MCP tool so it satisfies `AgentToolProtocol` inside `AgentExecutor`."""
139
+
140
+ def __init__(
141
+ self,
142
+ spec: McpToolSpec,
143
+ bridge: McpClientBridge,
144
+ *,
145
+ qualify_name: bool = False,
146
+ ) -> None:
147
+ self.spec = spec
148
+ self.bridge = bridge
149
+ self.name: str = f"mcp_{spec.server_id}_{spec.name}" if (qualify_name and spec.server_id) else spec.name
150
+ self.description: str = f"[MCP:{spec.server_id or 'server'}] {spec.description}"
151
+ self.parameters_schema: Mapping[str, Any] = spec.input_schema
152
+
153
+ def execute(self, params: Any, context: AgentExecutionContext) -> AgentToolResult:
154
+ arguments: Mapping[str, Any]
155
+ if isinstance(params, Mapping):
156
+ arguments = dict(params)
157
+ elif isinstance(params, str):
158
+ s = params.strip()
159
+ if s.startswith('{'):
160
+ try:
161
+ arguments = json.loads(s)
162
+ except Exception:
163
+ arguments = {'query': s}
164
+ else:
165
+ arguments = {'query': s}
166
+ else:
167
+ arguments = {}
168
+
169
+ res = self.bridge.call_tool(self.spec.server_id, self.spec.name, arguments)
170
+ if not res.get('ok', False):
171
+ return {
172
+ 'error': str(res.get('error') or 'MCP tool call failed'),
173
+ 'fuel_consumed': int(res.get('fuel_consumed', 75)),
174
+ }
175
+ return {
176
+ 'output': res.get('output'),
177
+ 'fuel_consumed': int(res.get('fuel_consumed', 120)),
178
+ }
179
+
180
+
181
+ class McpClientBridge:
182
+ """Manages local & remote Model Context Protocol (MCP) servers and dispatches tool/resource calls."""
183
+
184
+ def __init__(self, timeout_seconds: float = 15.0) -> None:
185
+ self.timeout_seconds = timeout_seconds
186
+ self._servers: dict[str, McpServerConfig] = {}
187
+ self._local_tool_handlers: dict[tuple[str, str], Callable[[Mapping[str, Any]], Any]] = {}
188
+ self._local_resource_handlers: dict[tuple[str, str], Callable[[str], Any]] = {}
189
+
190
+ def register_server(self, server: McpServerConfig) -> McpServerConfig:
191
+ self._servers[server.server_id] = server
192
+ return server
193
+
194
+ def remove_server(self, server_id: str) -> bool:
195
+ return self._servers.pop(server_id, None) is not None
196
+
197
+ def get_server(self, server_id: str) -> Optional[McpServerConfig]:
198
+ return self._servers.get(server_id)
199
+
200
+ def list_servers(self, *, enabled_only: bool = False) -> list[McpServerConfig]:
201
+ items = list(self._servers.values())
202
+ if enabled_only:
203
+ items = [s for s in items if s.enabled]
204
+ return items
205
+
206
+ def register_local_tool_handler(
207
+ self,
208
+ server_id: str,
209
+ tool_name: str,
210
+ handler: Callable[[Mapping[str, Any]], Any],
211
+ ) -> None:
212
+ self._local_tool_handlers[(server_id, tool_name)] = handler
213
+
214
+ def register_local_resource_handler(
215
+ self,
216
+ server_id: str,
217
+ uri: str,
218
+ handler: Callable[[str], Any],
219
+ ) -> None:
220
+ self._local_resource_handlers[(server_id, uri)] = handler
221
+
222
+ def _jsonrpc_request(
223
+ self,
224
+ server: McpServerConfig,
225
+ method: str,
226
+ params: Optional[Mapping[str, Any]] = None,
227
+ ) -> dict[str, Any]:
228
+ if not server.endpoint or not server.endpoint.startswith(('http://', 'https://')):
229
+ raise ValueError(f"MCP server '{server.server_id}' has no valid HTTP/HTTPS endpoint")
230
+ req_id = f"mcp-{uuid.uuid4().hex[:8]}"
231
+ rpc_body = {
232
+ 'jsonrpc': '2.0',
233
+ 'id': req_id,
234
+ 'method': method,
235
+ 'params': dict(params or {}),
236
+ }
237
+ raw_bytes, _ = netfetch.fetch(
238
+ server.endpoint,
239
+ max_bytes=4 * 1024 * 1024,
240
+ timeout=self.timeout_seconds,
241
+ method='POST',
242
+ data=json.dumps(rpc_body).encode('utf-8'),
243
+ headers=server.build_headers(),
244
+ allow_private=True,
245
+ )
246
+ text = raw_bytes.decode('utf-8', errors='replace').strip()
247
+ # Handle SSE-wrapped JSON-RPC responses (`data: {...}`)
248
+ if text.startswith('event:') or text.startswith('data:'):
249
+ for line in text.splitlines():
250
+ line_s = line.strip()
251
+ if line_s.startswith('data:'):
252
+ candidate = line_s[5:].strip()
253
+ if candidate.startswith('{'):
254
+ text = candidate
255
+ break
256
+ parsed = json.loads(text)
257
+ if not isinstance(parsed, dict):
258
+ raise ValueError('MCP server returned non-object JSON-RPC payload')
259
+ if 'error' in parsed and parsed['error']:
260
+ err_obj = parsed['error']
261
+ msg = err_obj.get('message') if isinstance(err_obj, Mapping) else str(err_obj)
262
+ raise ValueError(f"MCP JSON-RPC error: {msg}")
263
+ res = parsed.get('result')
264
+ return dict(res) if isinstance(res, Mapping) else {'value': res}
265
+
266
+ def sync_server_capabilities(self, server_id: str) -> McpServerConfig:
267
+ """Discover `tools/list` and `resources/list` from an MCP server."""
268
+ server = self._servers.get(server_id)
269
+ if server is None:
270
+ raise KeyError(f"Unknown MCP server: {server_id}")
271
+ now_ts = int(time.time())
272
+ if server.transport == 'builtin_mesh':
273
+ server.status = 'connected'
274
+ server.last_synced_at = now_ts
275
+ return server
276
+
277
+ try:
278
+ tools_res = self._jsonrpc_request(server, 'tools/list', {})
279
+ raw_tools = tools_res.get('tools') or []
280
+ discovered_tools = tuple(
281
+ McpToolSpec.from_mapping(t, server_id=server.server_id)
282
+ for t in raw_tools
283
+ if isinstance(t, Mapping) and t.get('name')
284
+ )
285
+ resources_res: Mapping[str, Any] = {}
286
+ try:
287
+ resources_res = self._jsonrpc_request(server, 'resources/list', {})
288
+ except Exception:
289
+ resources_res = {}
290
+ raw_resources = resources_res.get('resources') or []
291
+ discovered_resources = tuple(
292
+ McpResourceSpec.from_mapping(r, server_id=server.server_id)
293
+ for r in raw_resources
294
+ if isinstance(r, Mapping) and r.get('uri')
295
+ )
296
+ if discovered_tools:
297
+ server.tools = discovered_tools
298
+ if discovered_resources:
299
+ server.resources = discovered_resources
300
+ server.status = 'connected'
301
+ server.last_synced_at = now_ts
302
+ except Exception as exc:
303
+ server.status = f'error: {exc}'
304
+ server.last_synced_at = now_ts
305
+ return server
306
+
307
+ def call_tool(
308
+ self,
309
+ server_id: str,
310
+ tool_name: str,
311
+ arguments: Optional[Mapping[str, Any]] = None,
312
+ ) -> dict[str, Any]:
313
+ """Invoke a tool on a registered MCP server (local handler or remote JSON-RPC `tools/call`)."""
314
+ t0 = time.time()
315
+ args = dict(arguments or {})
316
+ server = self._servers.get(server_id)
317
+ if server is None:
318
+ # Search across enabled servers if server_id was omitted or generic
319
+ for candidate in self._servers.values():
320
+ if candidate.enabled and any(t.name == tool_name for t in candidate.tools):
321
+ server = candidate
322
+ server_id = candidate.server_id
323
+ break
324
+ if server is None:
325
+ return {
326
+ 'ok': False,
327
+ 'server_id': server_id,
328
+ 'tool_name': tool_name,
329
+ 'error': f"MCP server '{server_id}' not found",
330
+ 'duration_ms': int((time.time() - t0) * 1000),
331
+ 'fuel_consumed': 25,
332
+ }
333
+ if not server.enabled:
334
+ return {
335
+ 'ok': False,
336
+ 'server_id': server_id,
337
+ 'tool_name': tool_name,
338
+ 'error': f"MCP server '{server.name}' is currently disabled",
339
+ 'duration_ms': int((time.time() - t0) * 1000),
340
+ 'fuel_consumed': 10,
341
+ }
342
+
343
+ local_fn = self._local_tool_handlers.get((server_id, tool_name))
344
+ if local_fn is not None:
345
+ try:
346
+ out = local_fn(args)
347
+ dur_ms = max(1, int((time.time() - t0) * 1000))
348
+ return {
349
+ 'ok': True,
350
+ 'server_id': server_id,
351
+ 'server_name': server.name,
352
+ 'tool_name': tool_name,
353
+ 'arguments': args,
354
+ 'output': out,
355
+ 'duration_ms': dur_ms,
356
+ 'fuel_consumed': 120,
357
+ }
358
+ except Exception as exc:
359
+ dur_ms = max(1, int((time.time() - t0) * 1000))
360
+ return {
361
+ 'ok': False,
362
+ 'server_id': server_id,
363
+ 'server_name': server.name,
364
+ 'tool_name': tool_name,
365
+ 'arguments': args,
366
+ 'error': str(exc),
367
+ 'duration_ms': dur_ms,
368
+ 'fuel_consumed': 40,
369
+ }
370
+
371
+ try:
372
+ rpc_res = self._jsonrpc_request(
373
+ server,
374
+ 'tools/call',
375
+ {'name': tool_name, 'arguments': args},
376
+ )
377
+ content_list = rpc_res.get('content')
378
+ if isinstance(content_list, Sequence) and not isinstance(content_list, (str, bytes)):
379
+ texts = []
380
+ for item in content_list:
381
+ if isinstance(item, Mapping):
382
+ if item.get('type') == 'text' and 'text' in item:
383
+ texts.append(str(item['text']))
384
+ else:
385
+ texts.append(json.dumps(dict(item)))
386
+ output_val: Any = '\n'.join(texts) if len(texts) == 1 else (texts or rpc_res)
387
+ else:
388
+ output_val = rpc_res
389
+ dur_ms = max(1, int((time.time() - t0) * 1000))
390
+ return {
391
+ 'ok': True,
392
+ 'server_id': server_id,
393
+ 'server_name': server.name,
394
+ 'tool_name': tool_name,
395
+ 'arguments': args,
396
+ 'output': output_val,
397
+ 'duration_ms': dur_ms,
398
+ 'fuel_consumed': 180,
399
+ }
400
+ except Exception as exc:
401
+ dur_ms = max(1, int((time.time() - t0) * 1000))
402
+ return {
403
+ 'ok': False,
404
+ 'server_id': server_id,
405
+ 'server_name': server.name,
406
+ 'tool_name': tool_name,
407
+ 'arguments': args,
408
+ 'error': str(exc),
409
+ 'duration_ms': dur_ms,
410
+ 'fuel_consumed': 50,
411
+ }
412
+
413
+ def read_resource(self, server_id: str, uri: str) -> dict[str, Any]:
414
+ """Read an MCP resource by URI from a local or remote MCP server."""
415
+ server = self._servers.get(server_id)
416
+ if server is None:
417
+ for candidate in self._servers.values():
418
+ if candidate.enabled and any(r.uri == uri for r in candidate.resources):
419
+ server = candidate
420
+ server_id = candidate.server_id
421
+ break
422
+ if server is None:
423
+ return {'ok': False, 'error': f"MCP server '{server_id}' not found", 'uri': uri}
424
+
425
+ local_fn = self._local_resource_handlers.get((server_id, uri))
426
+ if local_fn is not None:
427
+ try:
428
+ return {
429
+ 'ok': True,
430
+ 'server_id': server_id,
431
+ 'uri': uri,
432
+ 'contents': local_fn(uri),
433
+ }
434
+ except Exception as exc:
435
+ return {'ok': False, 'server_id': server_id, 'uri': uri, 'error': str(exc)}
436
+
437
+ try:
438
+ res = self._jsonrpc_request(server, 'resources/read', {'uri': uri})
439
+ return {'ok': True, 'server_id': server_id, 'uri': uri, 'contents': res.get('contents', res)}
440
+ except Exception as exc:
441
+ return {'ok': False, 'server_id': server_id, 'uri': uri, 'error': str(exc)}
442
+
443
+ def as_agent_tools(self, *, server_ids: Optional[Sequence[str]] = None) -> list[McpToolAdapter]:
444
+ """Return `McpToolAdapter` instances for all enabled MCP servers (or the specified subset)."""
445
+ allowed = set(server_ids) if server_ids is not None else None
446
+ adapters: list[McpToolAdapter] = []
447
+ for srv in self._servers.values():
448
+ if not srv.enabled:
449
+ continue
450
+ if allowed is not None and srv.server_id not in allowed:
451
+ continue
452
+ for t_spec in srv.tools:
453
+ adapters.append(McpToolAdapter(t_spec, self))
454
+ return adapters