msp-py 0.1.0a1__tar.gz

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 (92) hide show
  1. msp_py-0.1.0a1/LICENSE +21 -0
  2. msp_py-0.1.0a1/PKG-INFO +537 -0
  3. msp_py-0.1.0a1/README.md +509 -0
  4. msp_py-0.1.0a1/pyproject.toml +48 -0
  5. msp_py-0.1.0a1/setup.cfg +4 -0
  6. msp_py-0.1.0a1/src/msp/__init__.py +97 -0
  7. msp_py-0.1.0a1/src/msp/core/__init__.py +1 -0
  8. msp_py-0.1.0a1/src/msp/core/content.py +270 -0
  9. msp_py-0.1.0a1/src/msp/core/exposure.py +90 -0
  10. msp_py-0.1.0a1/src/msp/core/grammar.py +62 -0
  11. msp_py-0.1.0a1/src/msp/core/grants.py +297 -0
  12. msp_py-0.1.0a1/src/msp/core/manifest.py +448 -0
  13. msp_py-0.1.0a1/src/msp/core/node.py +1009 -0
  14. msp_py-0.1.0a1/src/msp/core/peer.py +153 -0
  15. msp_py-0.1.0a1/src/msp/core/preset.py +162 -0
  16. msp_py-0.1.0a1/src/msp/core/remote_tools.py +420 -0
  17. msp_py-0.1.0a1/src/msp/core/schema.py +197 -0
  18. msp_py-0.1.0a1/src/msp/core/signal.py +144 -0
  19. msp_py-0.1.0a1/src/msp/discovery/__init__.py +1 -0
  20. msp_py-0.1.0a1/src/msp/discovery/base.py +72 -0
  21. msp_py-0.1.0a1/src/msp/discovery/directory.py +532 -0
  22. msp_py-0.1.0a1/src/msp/discovery/manual.py +260 -0
  23. msp_py-0.1.0a1/src/msp/mcp/__init__.py +30 -0
  24. msp_py-0.1.0a1/src/msp/mcp/bridge.py +234 -0
  25. msp_py-0.1.0a1/src/msp/mcp/client.py +170 -0
  26. msp_py-0.1.0a1/src/msp/mcp/http.py +749 -0
  27. msp_py-0.1.0a1/src/msp/mcp/jsonrpc.py +70 -0
  28. msp_py-0.1.0a1/src/msp/mcp/server.py +253 -0
  29. msp_py-0.1.0a1/src/msp/mcp/stdio.py +135 -0
  30. msp_py-0.1.0a1/src/msp/patterns/__init__.py +10 -0
  31. msp_py-0.1.0a1/src/msp/patterns/base.py +58 -0
  32. msp_py-0.1.0a1/src/msp/patterns/multi_round.py +360 -0
  33. msp_py-0.1.0a1/src/msp/patterns/task.py +1167 -0
  34. msp_py-0.1.0a1/src/msp/patterns/task_store.py +90 -0
  35. msp_py-0.1.0a1/src/msp/presence/__init__.py +1 -0
  36. msp_py-0.1.0a1/src/msp/presence/base.py +56 -0
  37. msp_py-0.1.0a1/src/msp/presence/directory.py +198 -0
  38. msp_py-0.1.0a1/src/msp/presence/manual.py +113 -0
  39. msp_py-0.1.0a1/src/msp/protocol/__init__.py +1 -0
  40. msp_py-0.1.0a1/src/msp/protocol/matcher.py +36 -0
  41. msp_py-0.1.0a1/src/msp/protocol/metrics.py +159 -0
  42. msp_py-0.1.0a1/src/msp/protocol/protocol.py +1143 -0
  43. msp_py-0.1.0a1/src/msp/transport/__init__.py +1 -0
  44. msp_py-0.1.0a1/src/msp/transport/_common.py +38 -0
  45. msp_py-0.1.0a1/src/msp/transport/base.py +122 -0
  46. msp_py-0.1.0a1/src/msp/transport/registry.py +92 -0
  47. msp_py-0.1.0a1/src/msp/transport/tcp.py +196 -0
  48. msp_py-0.1.0a1/src/msp/transport/uds.py +143 -0
  49. msp_py-0.1.0a1/src/msp/transport/websocket.py +433 -0
  50. msp_py-0.1.0a1/src/msp/utils/__init__.py +1 -0
  51. msp_py-0.1.0a1/src/msp/utils/atomic.py +46 -0
  52. msp_py-0.1.0a1/src/msp/utils/manual_source.py +138 -0
  53. msp_py-0.1.0a1/src/msp_py.egg-info/PKG-INFO +537 -0
  54. msp_py-0.1.0a1/src/msp_py.egg-info/SOURCES.txt +90 -0
  55. msp_py-0.1.0a1/src/msp_py.egg-info/dependency_links.txt +1 -0
  56. msp_py-0.1.0a1/src/msp_py.egg-info/requires.txt +5 -0
  57. msp_py-0.1.0a1/src/msp_py.egg-info/top_level.txt +1 -0
  58. msp_py-0.1.0a1/tests/test_connect_policy.py +86 -0
  59. msp_py-0.1.0a1/tests/test_discovery_robustness.py +212 -0
  60. msp_py-0.1.0a1/tests/test_exposure.py +103 -0
  61. msp_py-0.1.0a1/tests/test_fix_batch1.py +318 -0
  62. msp_py-0.1.0a1/tests/test_fix_batch2.py +311 -0
  63. msp_py-0.1.0a1/tests/test_grants.py +277 -0
  64. msp_py-0.1.0a1/tests/test_handshake_timeout.py +105 -0
  65. msp_py-0.1.0a1/tests/test_listen_config.py +120 -0
  66. msp_py-0.1.0a1/tests/test_liveness.py +147 -0
  67. msp_py-0.1.0a1/tests/test_manual.py +236 -0
  68. msp_py-0.1.0a1/tests/test_mcp.py +207 -0
  69. msp_py-0.1.0a1/tests/test_mcp_bridge_guards.py +256 -0
  70. msp_py-0.1.0a1/tests/test_mcp_http.py +269 -0
  71. msp_py-0.1.0a1/tests/test_mcp_http_client.py +258 -0
  72. msp_py-0.1.0a1/tests/test_metrics.py +254 -0
  73. msp_py-0.1.0a1/tests/test_multi_round.py +133 -0
  74. msp_py-0.1.0a1/tests/test_pattern_metadata.py +161 -0
  75. msp_py-0.1.0a1/tests/test_peer.py +135 -0
  76. msp_py-0.1.0a1/tests/test_presence_contract.py +118 -0
  77. msp_py-0.1.0a1/tests/test_reconnect.py +116 -0
  78. msp_py-0.1.0a1/tests/test_remote_tools.py +421 -0
  79. msp_py-0.1.0a1/tests/test_replace_grace.py +192 -0
  80. msp_py-0.1.0a1/tests/test_risk_fixes.py +357 -0
  81. msp_py-0.1.0a1/tests/test_robustness.py +213 -0
  82. msp_py-0.1.0a1/tests/test_schema.py +201 -0
  83. msp_py-0.1.0a1/tests/test_schema_source.py +109 -0
  84. msp_py-0.1.0a1/tests/test_settle.py +170 -0
  85. msp_py-0.1.0a1/tests/test_spec_links.py +125 -0
  86. msp_py-0.1.0a1/tests/test_task.py +322 -0
  87. msp_py-0.1.0a1/tests/test_task_guards.py +91 -0
  88. msp_py-0.1.0a1/tests/test_task_store.py +236 -0
  89. msp_py-0.1.0a1/tests/test_uds.py +136 -0
  90. msp_py-0.1.0a1/tests/test_version_consistency.py +70 -0
  91. msp_py-0.1.0a1/tests/test_websocket.py +126 -0
  92. msp_py-0.1.0a1/tests/test_wildcard.py +150 -0
msp_py-0.1.0a1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 [MSP Contributors]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,537 @@
1
+ Metadata-Version: 2.4
2
+ Name: msp-py
3
+ Version: 0.1.0a1
4
+ Summary: Model Sensing Protocol — a node-centric paradigm for decentralized AI communication.
5
+ Author: hxh230802
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/hxh230802/MSP
8
+ Project-URL: Repository, https://github.com/hxh230802/MSP
9
+ Project-URL: Issues, https://github.com/hxh230802/MSP/issues
10
+ Project-URL: Changelog, https://github.com/hxh230802/MSP/blob/master/CHANGELOG.md
11
+ Keywords: ai,protocol,signal,mcp,decentralized,multi-agent
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: watchdog<7,>=4.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: PyYAML>=6; extra == "dev"
26
+ Requires-Dist: jsonschema>=4; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # MSP — Model Sensing Protocol
30
+
31
+ A node-centric paradigm for decentralized AI communication.
32
+
33
+ Everything is a node.
34
+ Everything is a signal.
35
+ Every layer is extensible.
36
+
37
+ [中文](README.zh-CN.md)
38
+
39
+ ## Status
40
+
41
+ Early prototype. Core layers implemented:
42
+
43
+ - `core/` Signal, Manifest, ContentBlock, Preset, MSPNode
44
+ - `discovery/` Shared-directory discovery
45
+ - `presence/` Shared-directory advertisement
46
+ - `transport/` TCP backend with pluggable registry
47
+ - `protocol/` Handshake, broadcast, reqid-based tool calling
48
+ - `patterns/` Pluggable patterns (multi-round)
49
+ - `mcp/` MCP interop adapter (stdio, tools) — realizing "MCP ⊆ MSP"
50
+
51
+ ## Architecture
52
+
53
+ > The normative specification lives in [docs/spec/](docs/spec/README.md); design rationale in [docs/DESIGN.md](docs/DESIGN.md); the problem statement and positioning in [docs/whitepaper.md](docs/whitepaper.md).
54
+
55
+ ```text
56
+ ╔══════════════════════════════════════════════════════════════════════════╗
57
+ ║ MSP Overall Architecture ║
58
+ ║ Model Sensing Protocol ║
59
+ ║ ║
60
+ ║ Everything is a node. Everything is a signal. ║
61
+ ╚══════════════════════════════════════════════════════════════════════════╝
62
+
63
+ ┌──────────────────────────────────────────────────────────────────────────┐
64
+ │ Application Layer │
65
+ │ ┌────────────────────────────────────────────────────────────────────┐ │
66
+ │ │ MSPNode │ │
67
+ │ │ │ │
68
+ │ │ @node.tool("name") declare a tool │ │
69
+ │ │ @node.signal_source(p) declare a signal-source generator │ │
70
+ │ │ node.declare_signal(p) declare a signal path (no emission) │ │
71
+ │ │ @node.on(path) register a signal callback │ │
72
+ │ │ node.publish(p, d) publish a signal manually │ │
73
+ │ │ node.ensure_connected(p) dial a peer on demand (lazy policy) │ │
74
+ │ │ node.settle(t) wait for in-flight dials to land │ │
75
+ │ │ node.call(peer, t, a) call a tool synchronously │ │
76
+ │ │ node.register_pattern(h) register a communication pattern │ │
77
+ │ └────────────────────────────────────────────────────────────────────┘ │
78
+ └──────────────────────────────────────────────────────────────────────────┘
79
+ │
80
+ ▼
81
+ ┌──────────────────────────────────────────────────────────────────────────┐
82
+ │ Pattern Layer │
83
+ │ ┌────────────────────────────────────────────────────────────────────┐ │
84
+ │ │ PatternHandler abstract base class │ │
85
+ │ │ pattern_id · fallback_to · conflicts_with │ │
86
+ │ │ on_signal(protocol, peer_id, sig) │ │
87
+ │ └────────────────────────────────────────────────────────────────────┘ │
88
+ │ │
89
+ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌──────────────┐ │
90
+ │ │ Request-Reply │ │ PubSub │ │ MultiRound │ │ Streaming │ │
91
+ │ │ (core pattern)│ │ (core pattern)│ │ (this round) │ │ (future) │ │
92
+ │ │ │ │ │ │ │ │ │ │
93
+ │ │ tool_call │ │ signal │ │ tool_call │ │ stream_start │ │
94
+ │ │ tool_result │ │ │ │ tool_result │ │ stream_chunk │ │
95
+ │ │ │ │ │ │ input_required│ │ stream_end │ │
96
+ │ │ │ │ │ │ input_response│ │ │ │
97
+ │ └───────────────┘ └───────────────┘ └───────────────┘ └──────────────┘ │
98
+ │ │
99
+ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
100
+ │ │ TaskLifecycle │ │ CallbackPush │ │ AsyncPolling │ │
101
+ │ │ (future) │ │ (future) │ │ (future) │ │
102
+ │ └───────────────┘ └───────────────┘ └───────────────┘ │
103
+ │ ... (future extension) │
104
+ │ │
105
+ │ Pluggable · standalone files · keeps the core clean │
106
+ └──────────────────────────────────────────────────────────────────────────┘
107
+ │
108
+ ▼
109
+ ┌──────────────────────────────────────────────────────────────────────────┐
110
+ │ Protocol Layer │
111
+ │ MSPProtocol │
112
+ │ ┌────────────────────────────────────────────────────────────────────┐ │
113
+ │ │ Core signals (handled by the protocol layer) │ │
114
+ │ │ HELLO · ACK · PING · PONG · ERROR │ │
115
+ │ └────────────────────────────────────────────────────────────────────┘ │
116
+ │ ┌────────────────────────────────────────────────────────────────────┐ │
117
+ │ │ Signal routing │ │
118
+ │ │ _dispatch(peer_id, sig) │ │
119
+ │ │ if sig.pattern: → patterns[sig.pattern].on_signal(...) │ │
120
+ │ │ else: → _dispatch_core(...) │ │
121
+ │ └────────────────────────────────────────────────────────────────────┘ │
122
+ │ ┌────────────────────────────────────────────────────────────────────┐ │
123
+ │ │ Connection management │ │
124
+ │ │ connections · peer_manifests · pending_calls │ │
125
+ │ │ handlers · _on_connected · _on_disconnected │ │
126
+ │ └────────────────────────────────────────────────────────────────────┘ │
127
+ │ ┌────────────────────────────────────────────────────────────────────┐ │
128
+ │ │ Encoding │ │
129
+ │ │ _encode(Signal) → bytes · _decode(bytes) → dict │ │
130
+ │ └────────────────────────────────────────────────────────────────────┘ │
131
+ └──────────────────────────────────────────────────────────────────────────┘
132
+ │
133
+ ▼
134
+ ┌──────────────────────────────────────────────────────────────────────────┐
135
+ │ Transport Layer │
136
+ │ ┌────────────────────────────────────────────────────────────────────┐ │
137
+ │ │ TransportRegistry │ │
138
+ │ │ register(type, transport) · connect(endpoint) · listen_all() │ │
139
+ │ └────────────────────────────────────────────────────────────────────┘ │
140
+ │ │
141
+ │ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │
142
+ │ │ TCPTransport │ │ WebSocketTransport │ │ QUICTransport │ │
143
+ │ │ (implemented) │ │ (implemented) │ │ (future) │ │
144
+ │ │ │ │ │ │ │ │
145
+ │ │ length-prefix frame│ │ native msg framing │ │ stream framing │ │
146
+ │ │ send/recv │ │ send/recv │ │ send/recv │ │
147
+ │ └────────────────────┘ └────────────────────┘ └────────────────────┘ │
148
+ │ │
149
+ │ Unified interface: Connection.send(bytes) / receive() -> bytes │
150
+ │ Only moves bytes · no message semantics │
151
+ └──────────────────────────────────────────────────────────────────────────┘
152
+ │
153
+ ▼
154
+ ┌──────────────────────────────────────────────────────────────────────────┐
155
+ │ Discovery / Presence │
156
+ │ │
157
+ │ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │
158
+ │ │ Discovery │ │ Presence │ │
159
+ │ │ (how to find others) │ │ (how to be found) │ │
160
+ │ │ │ │ │ │
161
+ │ │ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │ │
162
+ │ │ │ DirectoryScan │ │ │ │ DirectoryWrite │ │ │
163
+ │ │ │ shared-dir scan │ │ │ │ shared-dir write │ │ │
164
+ │ │ │ (implemented) │ │ │ │ (implemented) │ │ │
165
+ │ │ └──────────────────────┘ │ │ └──────────────────────┘ │ │
166
+ │ │ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │ │
167
+ │ │ │ ManualConnect │ │ │ │ ManualAdvertise │ │ │
168
+ │ │ │ manual address │ │ │ │ manual advertise │ │ │
169
+ │ │ │ (implemented) │ │ │ │ (implemented) │ │ │
170
+ │ │ └──────────────────────┘ │ │ └──────────────────────┘ │ │
171
+ │ │ ┌──────────────────────┐ │ │ ┌──────────────────────┐ │ │
172
+ │ │ │ DHTDiscovery │ │ │ │ DHTAdvertise │ │ │
173
+ │ │ │ DHT lookup │ │ │ │ DHT register │ │ │
174
+ │ │ │ (future) │ │ │ │ (future) │ │ │
175
+ │ │ └──────────────────────┘ │ │ └──────────────────────┘ │ │
176
+ │ └──────────────────────────────┘ └──────────────────────────────┘ │
177
+ │ │
178
+ │ Orthogonal · freely composable │
179
+ │ Unified contract: Endpoint = { transport_type, address, node_id } │
180
+ └──────────────────────────────────────────────────────────────────────────┘
181
+
182
+ ═══════════════════════════════════════════════════════════════════════════
183
+ Data flow examples
184
+ ═══════════════════════════════════════════════════════════════════════════
185
+
186
+ Scenario 1: Tool call (request-reply pattern)
187
+
188
+ Client Server
189
+ │ │
190
+ │ 1. User: client.call(peer, tool, args)
191
+ │ │
192
+ │ ▼
193
+ │ 2. Pattern layer RequestReplyHandler
194
+ │ creates the tool_call signal
195
+ │ │
196
+ │ ▼
197
+ │ 3. Protocol layer _encode → JSON bytes
198
+ │ │
199
+ │ ▼
200
+ │ 4. Transport layer TCP + 4-byte length prefix
201
+ │ │
202
+ │─────────┼─────────────────────────────►│
203
+ │ │ 5. Transport layer reads
204
+ │ │
205
+ │ │ 6. Protocol layer _decode
206
+ │ │
207
+ │ │ 7. Pattern layer dispatches
208
+ │ │ runs the tool function
209
+ │ │
210
+ │◄────────┼─────────────────────────────│ 8. returns tool_result
211
+ │ │ │
212
+ │ 9. wakes the waiting call() │
213
+ │ │ │
214
+ │ 10. returns ToolResult │
215
+ ▼ ▼
216
+
217
+ Scenario 2: Signal push (broadcast)
218
+
219
+ Publisher Subscriber
220
+ │ │
221
+ │ 1. Generator yields {"status": "thinking"}
222
+ │ │
223
+ │ ▼
224
+ │ 2. Node auto-publishes ("ai.state", payload)
225
+ │ │
226
+ │ ▼
227
+ │ 3. Broadcasts the signal to every connected peer
228
+ │─────────┼─────────────────────────────►│
229
+ │ │ 4. Receives the signal
230
+ │ │
231
+ │ │ 5. Filters locally: path matches
232
+ │ │ "ai.state" → triggers
233
+ │ │ @on("ai.state") callback
234
+ ▼ ▼
235
+
236
+ Scenario 3: Multi-round (MultiRound pattern)
237
+
238
+ Client Server
239
+ │ │
240
+ │ ── tool_call(reqid=1) ──────────────►│
241
+ │ │ Tool running, missing arg
242
+ │ ◄── input_required(reqid=1) ─────────│ "Departure date?"
243
+ │ │
244
+ │ (client calls input_fn for the answer) │
245
+ │ │
246
+ │ ── input_response(reqid=1) ─────────►│ "2026-10-15"
247
+ │ │ Tool continues
248
+ │ │
249
+ │ ◄── input_required(reqid=1) ─────────│ "Preferred airline?"
250
+ │ ── input_response(reqid=1) ─────────►│ "Air China"
251
+ │ │ Tool completes
252
+ │ ◄── tool_result(reqid=1) ────────────│ Final result
253
+ ▼ ▼
254
+
255
+ ═══════════════════════════════════════════════════════════════════════════
256
+ Core data structures
257
+ ═══════════════════════════════════════════════════════════════════════════
258
+
259
+ ┌─────────────────────────────────────────────────────────────────────────┐
260
+ │ Signal │
261
+ │ ┌───────────────────────────────────────────────────────────────────┐ │
262
+ │ │ type: "tool_call" | "signal" | "hello" | ... │ │
263
+ │ │ source: "node_a" │ │
264
+ │ │ pattern: "msp/multi-round" | None ← routing key │ │
265
+ │ │ target: "node_b" | None │ │
266
+ │ │ path: "ai.state" | None ← for pub-sub │ │
267
+ │ │ reqid: "mr-1" | None ← for request-reply │ │
268
+ │ │ payload: {...} │ │
269
+ │ │ timestamp: 1712345678.123 │ │
270
+ │ └───────────────────────────────────────────────────────────────────┘ │
271
+ └─────────────────────────────────────────────────────────────────────────┘
272
+
273
+ ┌─────────────────────────────────────────────────────────────────────────┐
274
+ │ Manifest (node capability list) │
275
+ │ ┌───────────────────────────────────────────────────────────────────┐ │
276
+ │ │ version: 3 │ │
277
+ │ │ patterns: ["msp/request-reply", "msp/pubsub", │ │
278
+ │ │ "msp/multi-round"] │ │
279
+ │ │ signals: {path: SignalDescriptor} │ │
280
+ │ │ tools: {name: ToolDescriptor} │ │
281
+ │ │ resources: {uri: ResourceDescriptor} │ │
282
+ │ │ prompts: {name: PromptDescriptor} │ │
283
+ │ │ clientCapabilities: {sampling, elicitation, roots} │ │
284
+ │ └───────────────────────────────────────────────────────────────────┘ │
285
+ └─────────────────────────────────────────────────────────────────────────┘
286
+
287
+ ┌─────────────────────────────────────────────────────────────────────────┐
288
+ │ Endpoint (node transport endpoint) │
289
+ │ ┌───────────────────────────────────────────────────────────────────┐ │
290
+ │ │ transport_type: "tcp" | "websocket" | "quic" │ │
291
+ │ │ address: {"host": "127.0.0.1", "port": 45678} │ │
292
+ │ │ node_id: "node_a" │ │
293
+ │ └───────────────────────────────────────────────────────────────────┘ │
294
+ └─────────────────────────────────────────────────────────────────────────┘
295
+
296
+ ═══════════════════════════════════════════════════════════════════════════
297
+ MCP comparison
298
+ ═══════════════════════════════════════════════════════════════════════════
299
+
300
+ ┌──────────────────────────┬─────────────────────────────────────────┐
301
+ │ MCP │ MSP │
302
+ ├──────────────────────────┼─────────────────────────────────────────┤
303
+ │ Client-Host-Server │ Peer-to-peer nodes │
304
+ │ Manual server config │ Auto-discovery │
305
+ │ Request-reply │ Request-reply + pub-sub + multi-round + ...│
306
+ │ Request/response + notif.│ Full-duplex │
307
+ │ Protocol-level ext. (SEP)│ Pluggable patterns (PatternHandler) │
308
+ │ Large core changes │ Core changes ≤ 60 lines │
309
+ │ Tool-centric │ Node-centric │
310
+ │ Everything is a tool │ Everything is a node │
311
+ └──────────────────────────┴─────────────────────────────────────────┘
312
+
313
+ ┌───────────────────────────────────────────────────────────────────┐
314
+ │ │
315
+ │ MCP ⊆ MSP │
316
+ │ │
317
+ │ MCP is a special case of MSP in the "tool call" scenario │
318
+ │ │
319
+ └───────────────────────────────────────────────────────────────────┘
320
+ ```
321
+
322
+ ### Startup timing: `start(settle=...)`
323
+
324
+ `start()` blocks until the network has settled — at most `settle_timeout`
325
+ (500 ms by default):
326
+
327
+ ```python
328
+ node.start() # settle=True by default: peers are connected on return
329
+ ok = node.start(settle_timeout=1.0) # custom bound; False means dials were still in flight
330
+ node.start(settle=False) # purely non-blocking: connections form in background
331
+
332
+ node.settle(0.5) # or wait separately; returns whether it settled in time
333
+ ```
334
+
335
+ Why it exists: **discovery is synchronous, dialing is asynchronous.** The
336
+ directory scan runs inside `start()` (neighbors land in the neighbor table
337
+ immediately), but dials happen on background threads. So a `publish()` right
338
+ after `start()` fans out to an empty connection set — **silently dropped, no
339
+ error**. `settle` collapses that window.
340
+
341
+ Boundaries (honest version):
342
+
343
+ - **Waits, never initiates**: `settle()` never calls `ensure_connected()`. On a
344
+ `connect_policy="lazy"` node it is a no-op — it returns `True` immediately and
345
+ **never connects a peer that lazy chose not to connect**.
346
+ - Covers **discovery-driven** dials only; connections started by `call()` /
347
+ `fetch_manifest()` / `msp/task` are not counted.
348
+ - Does not promise "every future node is connected": nodes that announce after
349
+ `start()` returned go through normal discovery.
350
+ - A timeout is a bound, not an error: `False` only means some dial had not
351
+ landed yet, and the node keeps working.
352
+
353
+ ### Peer view
354
+
355
+ A peer's information is spread across several places — its `Endpoint`
356
+ (on the node), and its `Connection` and manifest (on the protocol
357
+ layer). `Peer` aggregates them into one read-only object.
358
+
359
+ `Peer` is a *view*, not a *store*: it holds no state, and every
360
+ attribute reads live from the owning node. When a peer updates its
361
+ manifest or drops the connection, the same `Peer` object reflects it
362
+ immediately — no copy to keep in sync.
363
+
364
+ ```python
365
+ peer = node.peer("node_a")
366
+ peer.is_connected # bool
367
+ peer.manifest # dict | None
368
+ peer.tools() # ["get_weather", ...]
369
+ peer.signals() # ["ai.state", ...]
370
+ peer.supports("msp/multi-round") # bool
371
+
372
+ for p in node.connected_peers():
373
+ if p.supports("msp/multi-round"):
374
+ ...
375
+ ```
376
+
377
+ Operations (`call`, `publish`) stay on the node — `Peer`
378
+ is query-only. `publish` is a broadcast, not peer-scoped, so it does
379
+ not belong on `Peer`.
380
+
381
+ ### Calling a peer's tools
382
+
383
+ `inputSchema` travels in the manifest, so a peer's tools can be turned
384
+ into local callables — no code-generation step, and nothing new on the wire:
385
+
386
+ ```python
387
+ peer = node.peer("node_a")
388
+
389
+ peer.t.get_weather(location="Beijing") # -> ToolResult
390
+ peer.t.get_weather(location="Beijing").unwrap() # -> {'temp': 22, ...}; raises on error
391
+ peer.t["get_weather"](location="Beijing") # dynamic / dotted names
392
+ peer.t.tool("tools") # escape hatch for collisions
393
+
394
+ print(peer.t.signature("get_weather")) # (location: str, *, days: int = <optional>)
395
+ print(peer.t.describe("get_weather")) # description + per-argument docs
396
+ peer.t.names() # what the peer declares
397
+ peer.t.conflicts() # names not reachable as attributes
398
+ ```
399
+
400
+ `<optional>` is not a placeholder for "no such thing": it means the schema marks
401
+ the argument optional but declares **no default**, so omitting it sends nothing
402
+ and the provider applies its own default. (`schema_from_func` never emits
403
+ `default`, which is why a Python default of `3` shows up as `<optional>` rather
404
+ than `3` — see the DESIGN notes for why we degrade instead of guessing.)
405
+
406
+ Three properties are deliberate:
407
+
408
+ - **No shadowing.** Tools live under `peer.t`, not on `Peer` itself. `Peer`
409
+ already has `tools` / `signals` / `manifest` / `node_id`, and tool names may
410
+ collide with them; `__getattr__` never fires for an existing attribute, so
411
+ hanging tools directly on `Peer` would silently return the wrong thing.
412
+ Colliding tools stay reachable through `peer.t["..."]` / `peer.t.tool(...)`.
413
+ - **Attribute access is pure.** `peer.t.anything` does not dial, does not read
414
+ the manifest, and keeps no state; `ensure_connected()` happens when you
415
+ *call*. Otherwise `hasattr`, `dir`, or a debugger expanding the object would
416
+ trigger network I/O — and `Peer` promises benign, side-effect-free queries.
417
+ - **Errors stay values.** A call returns `ToolResult`; use `.unwrap()` when you
418
+ want an exception. Wrong arguments are a caller-side programming error, so
419
+ they raise `TypeError` locally instead of becoming a remote error string.
420
+
421
+ Signature quality is bounded by `schema_from_func`, which currently emits no
422
+ `default` and silently drops pydantic v2 constraints; anything the schema does
423
+ not express degrades to `Any` rather than being guessed. Return values are
424
+ still `Any`, because `ToolDescriptor.outputTypes` is always empty today.
425
+
426
+ ### Extension hooks (install_api)
427
+
428
+ A layer component may define `install_api(node)`; MSPNode calls it (if
429
+ present) right after constructing the component, so the component can
430
+ attach convenience methods onto the node without changing the core.
431
+ Patterns use it for `node.multiround`; manual discovery uses it for
432
+ `node.add_manual_source(...)`.
433
+
434
+ ```python
435
+ node.start()
436
+ node.add_manual_source("http://registry2:8080/nodes") # 运行时动态加发现源
437
+ ```
438
+
439
+ ### MCP interop (adapter)
440
+
441
+ `msp.mcp` turns the "MCP ⊆ MSP" box above into working code: in the
442
+ tool-call scenario, an MSP node bridges the MCP ecosystem in both
443
+ directions — without touching the core.
444
+
445
+ - `MCPClient` — MSP acts as an MCP client, calling an MCP server directly
446
+ - `MCPServer` — expose an MSP node as an MCP server
447
+ - `mount_mcp_tools` — re-export a remote MCP server's tools as MSP tools
448
+
449
+ ```python
450
+ from msp.mcp import MCPClient, spawn, mount_mcp_tools
451
+
452
+ # MSP → MCP: call a local MCP server (spawned over stdio)
453
+ client = MCPClient(spawn(["npx", "-y", "@modelcontextprotocol/server-filesystem"]))
454
+ print(client.list_tools())
455
+ print(client.call_tool("read_file", {"path": "/tmp/a.txt"}))
456
+
457
+ # Bridge: mount that MCP server's tools on this node so other MSP peers can call them
458
+ node_a = MSPNode("node_a")
459
+ mount_mcp_tools(node_a, client)
460
+ node_b.call("node_a", "read_file", {"path": "/tmp/a.txt"})
461
+
462
+ # MCP → MSP: run node as an MCP server; an external MCP client spawns this process
463
+ # from msp.mcp import MCPServer, current_stdio
464
+ # MCPServer(node, current_stdio()).run()
465
+ ```
466
+
467
+ Current scope: stdio transport + tools capability (`initialize` /
468
+ `tools/list` / `tools/call`). Streamable HTTP and resources / prompts /
469
+ sampling come later.
470
+
471
+ ## ProReAct: waiting is a subscription, not an action
472
+
473
+ The classic ReAct loop is pull-based and turn-based: observation only
474
+ comes from tool returns, so "waiting" — a non-action — can only be faked
475
+ as an action. To wait for a download, a push, or a job, the agent can
476
+ only `sleep(45)` and check again; if not done, sleep and check again.
477
+ Polling is not a strategy here — it is a forced expression: the model
478
+ writes "not acting" as "acting", spending tokens and turns on "nothing
479
+ happened yet".
480
+
481
+ MSP moves "waiting" out of the loop and into the protocol layer:
482
+
483
+ ```python
484
+ @node.on("notify.web.download") # declare interest locally, then suspend
485
+ def on_done(sig):
486
+ # when the download truly finishes, the peer publishes the path,
487
+ # the broadcast reaches this node, and the event wakes the agent
488
+ ...
489
+ ```
490
+
491
+ Waiting is downgraded from an "action" to a "subscription" — declaring
492
+ interest locally instead of polling — and wake-up is passive. That is
493
+ exactly what "Sensing" in the protocol's name means: the protocol senses,
494
+ so the model doesn't have to poll.
495
+
496
+ ## Install
497
+
498
+ ```bash
499
+ pip install -e .
500
+ ```
501
+
502
+ 以「可编辑」模式安装,源码改动即时生效(建议在虚拟环境里执行)。
503
+
504
+ ## Run
505
+
506
+ ```bash
507
+ python -m msp.core.node
508
+ ```
509
+
510
+ ## Run tests(运行测试)
511
+
512
+ 测试是纯标准库 `unittest`(不 mock 网络:真 socket、真子进程),两种跑法任选其一。
513
+
514
+ 装过包(`pip install -e .`)之后,直接在仓库根目录跑:
515
+
516
+ ```bash
517
+ python -m unittest discover -s tests
518
+ ```
519
+
520
+ 不想安装、只想用源码树时,给 `PYTHONPATH` 指向 `src`:
521
+
522
+ ```bash
523
+ # Linux / macOS
524
+ PYTHONPATH=src python -m unittest discover -s tests
525
+
526
+ # Windows PowerShell
527
+ $env:PYTHONPATH="src"; python -m unittest discover -s tests
528
+ ```
529
+
530
+ 也可以只跑单个文件,例如 `python -m unittest tests.test_task`。
531
+
532
+ 注意:Windows 没有 `AF_UNIX`,`tests/test_uds.py` 里的 4 个 UDS 用例会自动
533
+ skip(不是失败)。
534
+
535
+ ## Acknowledgement
536
+
537
+ Thanks to Cordis, DSH, KnotLink, MCP and Glycoprotein.