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.
- msp_py-0.1.0a1/LICENSE +21 -0
- msp_py-0.1.0a1/PKG-INFO +537 -0
- msp_py-0.1.0a1/README.md +509 -0
- msp_py-0.1.0a1/pyproject.toml +48 -0
- msp_py-0.1.0a1/setup.cfg +4 -0
- msp_py-0.1.0a1/src/msp/__init__.py +97 -0
- msp_py-0.1.0a1/src/msp/core/__init__.py +1 -0
- msp_py-0.1.0a1/src/msp/core/content.py +270 -0
- msp_py-0.1.0a1/src/msp/core/exposure.py +90 -0
- msp_py-0.1.0a1/src/msp/core/grammar.py +62 -0
- msp_py-0.1.0a1/src/msp/core/grants.py +297 -0
- msp_py-0.1.0a1/src/msp/core/manifest.py +448 -0
- msp_py-0.1.0a1/src/msp/core/node.py +1009 -0
- msp_py-0.1.0a1/src/msp/core/peer.py +153 -0
- msp_py-0.1.0a1/src/msp/core/preset.py +162 -0
- msp_py-0.1.0a1/src/msp/core/remote_tools.py +420 -0
- msp_py-0.1.0a1/src/msp/core/schema.py +197 -0
- msp_py-0.1.0a1/src/msp/core/signal.py +144 -0
- msp_py-0.1.0a1/src/msp/discovery/__init__.py +1 -0
- msp_py-0.1.0a1/src/msp/discovery/base.py +72 -0
- msp_py-0.1.0a1/src/msp/discovery/directory.py +532 -0
- msp_py-0.1.0a1/src/msp/discovery/manual.py +260 -0
- msp_py-0.1.0a1/src/msp/mcp/__init__.py +30 -0
- msp_py-0.1.0a1/src/msp/mcp/bridge.py +234 -0
- msp_py-0.1.0a1/src/msp/mcp/client.py +170 -0
- msp_py-0.1.0a1/src/msp/mcp/http.py +749 -0
- msp_py-0.1.0a1/src/msp/mcp/jsonrpc.py +70 -0
- msp_py-0.1.0a1/src/msp/mcp/server.py +253 -0
- msp_py-0.1.0a1/src/msp/mcp/stdio.py +135 -0
- msp_py-0.1.0a1/src/msp/patterns/__init__.py +10 -0
- msp_py-0.1.0a1/src/msp/patterns/base.py +58 -0
- msp_py-0.1.0a1/src/msp/patterns/multi_round.py +360 -0
- msp_py-0.1.0a1/src/msp/patterns/task.py +1167 -0
- msp_py-0.1.0a1/src/msp/patterns/task_store.py +90 -0
- msp_py-0.1.0a1/src/msp/presence/__init__.py +1 -0
- msp_py-0.1.0a1/src/msp/presence/base.py +56 -0
- msp_py-0.1.0a1/src/msp/presence/directory.py +198 -0
- msp_py-0.1.0a1/src/msp/presence/manual.py +113 -0
- msp_py-0.1.0a1/src/msp/protocol/__init__.py +1 -0
- msp_py-0.1.0a1/src/msp/protocol/matcher.py +36 -0
- msp_py-0.1.0a1/src/msp/protocol/metrics.py +159 -0
- msp_py-0.1.0a1/src/msp/protocol/protocol.py +1143 -0
- msp_py-0.1.0a1/src/msp/transport/__init__.py +1 -0
- msp_py-0.1.0a1/src/msp/transport/_common.py +38 -0
- msp_py-0.1.0a1/src/msp/transport/base.py +122 -0
- msp_py-0.1.0a1/src/msp/transport/registry.py +92 -0
- msp_py-0.1.0a1/src/msp/transport/tcp.py +196 -0
- msp_py-0.1.0a1/src/msp/transport/uds.py +143 -0
- msp_py-0.1.0a1/src/msp/transport/websocket.py +433 -0
- msp_py-0.1.0a1/src/msp/utils/__init__.py +1 -0
- msp_py-0.1.0a1/src/msp/utils/atomic.py +46 -0
- msp_py-0.1.0a1/src/msp/utils/manual_source.py +138 -0
- msp_py-0.1.0a1/src/msp_py.egg-info/PKG-INFO +537 -0
- msp_py-0.1.0a1/src/msp_py.egg-info/SOURCES.txt +90 -0
- msp_py-0.1.0a1/src/msp_py.egg-info/dependency_links.txt +1 -0
- msp_py-0.1.0a1/src/msp_py.egg-info/requires.txt +5 -0
- msp_py-0.1.0a1/src/msp_py.egg-info/top_level.txt +1 -0
- msp_py-0.1.0a1/tests/test_connect_policy.py +86 -0
- msp_py-0.1.0a1/tests/test_discovery_robustness.py +212 -0
- msp_py-0.1.0a1/tests/test_exposure.py +103 -0
- msp_py-0.1.0a1/tests/test_fix_batch1.py +318 -0
- msp_py-0.1.0a1/tests/test_fix_batch2.py +311 -0
- msp_py-0.1.0a1/tests/test_grants.py +277 -0
- msp_py-0.1.0a1/tests/test_handshake_timeout.py +105 -0
- msp_py-0.1.0a1/tests/test_listen_config.py +120 -0
- msp_py-0.1.0a1/tests/test_liveness.py +147 -0
- msp_py-0.1.0a1/tests/test_manual.py +236 -0
- msp_py-0.1.0a1/tests/test_mcp.py +207 -0
- msp_py-0.1.0a1/tests/test_mcp_bridge_guards.py +256 -0
- msp_py-0.1.0a1/tests/test_mcp_http.py +269 -0
- msp_py-0.1.0a1/tests/test_mcp_http_client.py +258 -0
- msp_py-0.1.0a1/tests/test_metrics.py +254 -0
- msp_py-0.1.0a1/tests/test_multi_round.py +133 -0
- msp_py-0.1.0a1/tests/test_pattern_metadata.py +161 -0
- msp_py-0.1.0a1/tests/test_peer.py +135 -0
- msp_py-0.1.0a1/tests/test_presence_contract.py +118 -0
- msp_py-0.1.0a1/tests/test_reconnect.py +116 -0
- msp_py-0.1.0a1/tests/test_remote_tools.py +421 -0
- msp_py-0.1.0a1/tests/test_replace_grace.py +192 -0
- msp_py-0.1.0a1/tests/test_risk_fixes.py +357 -0
- msp_py-0.1.0a1/tests/test_robustness.py +213 -0
- msp_py-0.1.0a1/tests/test_schema.py +201 -0
- msp_py-0.1.0a1/tests/test_schema_source.py +109 -0
- msp_py-0.1.0a1/tests/test_settle.py +170 -0
- msp_py-0.1.0a1/tests/test_spec_links.py +125 -0
- msp_py-0.1.0a1/tests/test_task.py +322 -0
- msp_py-0.1.0a1/tests/test_task_guards.py +91 -0
- msp_py-0.1.0a1/tests/test_task_store.py +236 -0
- msp_py-0.1.0a1/tests/test_uds.py +136 -0
- msp_py-0.1.0a1/tests/test_version_consistency.py +70 -0
- msp_py-0.1.0a1/tests/test_websocket.py +126 -0
- 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.
|
msp_py-0.1.0a1/PKG-INFO
ADDED
|
@@ -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.
|