mcp-blackboard-graph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_blackboard_graph/__init__.py +0 -0
- mcp_blackboard_graph/main.py +354 -0
- mcp_blackboard_graph-0.1.0.dist-info/METADATA +6 -0
- mcp_blackboard_graph-0.1.0.dist-info/RECORD +7 -0
- mcp_blackboard_graph-0.1.0.dist-info/WHEEL +5 -0
- mcp_blackboard_graph-0.1.0.dist-info/entry_points.txt +2 -0
- mcp_blackboard_graph-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP server for multi-agent coordination via shared DAG.
|
|
4
|
+
|
|
5
|
+
Nodes have: id, parents, children, status, notes, messages, data.
|
|
6
|
+
Multiple agents can read/write to the graph.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from mcp.server import Server
|
|
16
|
+
from mcp.server.stdio import stdio_server
|
|
17
|
+
from mcp.types import Tool, TextContent
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_graph_path():
|
|
21
|
+
"""Get path to graph.json file."""
|
|
22
|
+
path = os.environ.get("BLACKBOARD_GRAPH_PATH")
|
|
23
|
+
if path:
|
|
24
|
+
return Path(path)
|
|
25
|
+
return Path.cwd() / "graph.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_graph():
|
|
29
|
+
"""Load graph from JSON file."""
|
|
30
|
+
path = get_graph_path()
|
|
31
|
+
if path.exists():
|
|
32
|
+
return json.loads(path.read_text())
|
|
33
|
+
return {
|
|
34
|
+
"nodes": {
|
|
35
|
+
"root": {
|
|
36
|
+
"id": "root",
|
|
37
|
+
"parents": [],
|
|
38
|
+
"children": [],
|
|
39
|
+
"status": "active",
|
|
40
|
+
"notes": [],
|
|
41
|
+
"messages": [],
|
|
42
|
+
"data": {}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def save_graph(graph):
|
|
49
|
+
"""Save graph to JSON file."""
|
|
50
|
+
path = get_graph_path()
|
|
51
|
+
path.write_text(json.dumps(graph, indent=2) + "\n")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_node(graph, node_id):
|
|
55
|
+
"""Get a node by id."""
|
|
56
|
+
return graph["nodes"].get(node_id)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ensure_node_fields(node):
|
|
60
|
+
"""Ensure node has all required fields."""
|
|
61
|
+
node.setdefault("parents", [])
|
|
62
|
+
node.setdefault("children", [])
|
|
63
|
+
node.setdefault("status", "active")
|
|
64
|
+
node.setdefault("notes", [])
|
|
65
|
+
node.setdefault("messages", [])
|
|
66
|
+
node.setdefault("data", {})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_node(node, show_notes=True, show_messages=True, show_data=True):
|
|
70
|
+
"""Format a single node for display."""
|
|
71
|
+
parts = [f"{node['id']} [{node.get('status', 'active')}]"]
|
|
72
|
+
if node.get("parents"):
|
|
73
|
+
parts.append(f" parents: {', '.join(node['parents'])}")
|
|
74
|
+
if node.get("children"):
|
|
75
|
+
parts.append(f" children: {', '.join(node['children'])}")
|
|
76
|
+
if show_notes and node.get("notes"):
|
|
77
|
+
parts.append(f" notes: {len(node['notes'])}")
|
|
78
|
+
for n in node["notes"]:
|
|
79
|
+
parts.append(f" - {n[:50]}{'...' if len(n) > 50 else ''}")
|
|
80
|
+
if show_messages and node.get("messages"):
|
|
81
|
+
parts.append(f" messages: {len(node['messages'])}")
|
|
82
|
+
for m in node["messages"]:
|
|
83
|
+
parts.append(f" - [{m.get('from', '?')}] {m['text'][:40]}{'...' if len(m['text']) > 40 else ''}")
|
|
84
|
+
if show_data and node.get("data"):
|
|
85
|
+
parts.append(f" data: {json.dumps(node['data'])[:60]}{'...' if len(json.dumps(node['data'])) > 60 else ''}")
|
|
86
|
+
return "\n".join(parts)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def format_graph(graph, node_id=None, show_notes=True, show_messages=True, show_data=True):
|
|
90
|
+
"""Format graph or single node for display."""
|
|
91
|
+
if node_id:
|
|
92
|
+
node = get_node(graph, node_id)
|
|
93
|
+
if not node:
|
|
94
|
+
return f"Error: node '{node_id}' not found"
|
|
95
|
+
return format_node(node, show_notes, show_messages, show_data)
|
|
96
|
+
|
|
97
|
+
lines = []
|
|
98
|
+
for nid, node in graph["nodes"].items():
|
|
99
|
+
lines.append(format_node(node, show_notes, show_messages, show_data))
|
|
100
|
+
lines.append("")
|
|
101
|
+
return "\n".join(lines).strip()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def run_server():
|
|
105
|
+
"""Run the MCP server."""
|
|
106
|
+
server = Server("mcp-blackboard-graph")
|
|
107
|
+
|
|
108
|
+
tools = [
|
|
109
|
+
Tool(
|
|
110
|
+
name="show",
|
|
111
|
+
description="Show the graph or a single node",
|
|
112
|
+
inputSchema={
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": {
|
|
115
|
+
"node": {"type": "string", "description": "Node id to show (omit for all)"},
|
|
116
|
+
"hide_notes": {"type": "boolean", "description": "Hide notes", "default": False},
|
|
117
|
+
"hide_messages": {"type": "boolean", "description": "Hide messages", "default": False},
|
|
118
|
+
"hide_data": {"type": "boolean", "description": "Hide data", "default": False}
|
|
119
|
+
},
|
|
120
|
+
"required": []
|
|
121
|
+
}
|
|
122
|
+
),
|
|
123
|
+
Tool(
|
|
124
|
+
name="add",
|
|
125
|
+
description="Add a new node to the graph",
|
|
126
|
+
inputSchema={
|
|
127
|
+
"type": "object",
|
|
128
|
+
"properties": {
|
|
129
|
+
"id": {"type": "string", "description": "Node id (unique)"},
|
|
130
|
+
"parents": {
|
|
131
|
+
"type": "array",
|
|
132
|
+
"items": {"type": "string"},
|
|
133
|
+
"description": "Parent node ids (for DAG structure)"
|
|
134
|
+
},
|
|
135
|
+
"status": {"type": "string", "description": "Initial status (default: active)"},
|
|
136
|
+
"data": {"type": "object", "description": "Initial data dict"}
|
|
137
|
+
},
|
|
138
|
+
"required": ["id"]
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
Tool(
|
|
142
|
+
name="remove",
|
|
143
|
+
description="Remove a node from the graph",
|
|
144
|
+
inputSchema={
|
|
145
|
+
"type": "object",
|
|
146
|
+
"properties": {
|
|
147
|
+
"id": {"type": "string", "description": "Node id to remove"}
|
|
148
|
+
},
|
|
149
|
+
"required": ["id"]
|
|
150
|
+
}
|
|
151
|
+
),
|
|
152
|
+
Tool(
|
|
153
|
+
name="get",
|
|
154
|
+
description="Get full details of a node as JSON",
|
|
155
|
+
inputSchema={
|
|
156
|
+
"type": "object",
|
|
157
|
+
"properties": {
|
|
158
|
+
"id": {"type": "string", "description": "Node id"}
|
|
159
|
+
},
|
|
160
|
+
"required": ["id"]
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
Tool(
|
|
164
|
+
name="set_status",
|
|
165
|
+
description="Set status of a node",
|
|
166
|
+
inputSchema={
|
|
167
|
+
"type": "object",
|
|
168
|
+
"properties": {
|
|
169
|
+
"id": {"type": "string", "description": "Node id"},
|
|
170
|
+
"status": {"type": "string", "description": "New status"}
|
|
171
|
+
},
|
|
172
|
+
"required": ["id", "status"]
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
Tool(
|
|
176
|
+
name="add_data",
|
|
177
|
+
description="Merge data dict into a node's data",
|
|
178
|
+
inputSchema={
|
|
179
|
+
"type": "object",
|
|
180
|
+
"properties": {
|
|
181
|
+
"id": {"type": "string", "description": "Node id"},
|
|
182
|
+
"data": {"type": "object", "description": "Data to merge"}
|
|
183
|
+
},
|
|
184
|
+
"required": ["id", "data"]
|
|
185
|
+
}
|
|
186
|
+
),
|
|
187
|
+
Tool(
|
|
188
|
+
name="add_note",
|
|
189
|
+
description="Add a note to a node",
|
|
190
|
+
inputSchema={
|
|
191
|
+
"type": "object",
|
|
192
|
+
"properties": {
|
|
193
|
+
"id": {"type": "string", "description": "Node id"},
|
|
194
|
+
"note": {"type": "string", "description": "Note text"}
|
|
195
|
+
},
|
|
196
|
+
"required": ["id", "note"]
|
|
197
|
+
}
|
|
198
|
+
),
|
|
199
|
+
Tool(
|
|
200
|
+
name="send_message",
|
|
201
|
+
description="Send a message to a node",
|
|
202
|
+
inputSchema={
|
|
203
|
+
"type": "object",
|
|
204
|
+
"properties": {
|
|
205
|
+
"id": {"type": "string", "description": "Node id"},
|
|
206
|
+
"message": {"type": "string", "description": "Message text"},
|
|
207
|
+
"from_agent": {"type": "string", "description": "Sender name (optional)"}
|
|
208
|
+
},
|
|
209
|
+
"required": ["id", "message"]
|
|
210
|
+
}
|
|
211
|
+
),
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
@server.list_tools()
|
|
215
|
+
async def list_tools():
|
|
216
|
+
return tools
|
|
217
|
+
|
|
218
|
+
@server.call_tool()
|
|
219
|
+
async def call_tool(name: str, arguments: dict):
|
|
220
|
+
graph = load_graph()
|
|
221
|
+
|
|
222
|
+
if name == "show":
|
|
223
|
+
node_id = arguments.get("node")
|
|
224
|
+
show_notes = not arguments.get("hide_notes", False)
|
|
225
|
+
show_messages = not arguments.get("hide_messages", False)
|
|
226
|
+
show_data = not arguments.get("hide_data", False)
|
|
227
|
+
result = format_graph(graph, node_id, show_notes, show_messages, show_data)
|
|
228
|
+
return [TextContent(type="text", text=result)]
|
|
229
|
+
|
|
230
|
+
elif name == "add":
|
|
231
|
+
node_id = arguments.get("id", "").strip()
|
|
232
|
+
parents = arguments.get("parents", [])
|
|
233
|
+
status = arguments.get("status", "active")
|
|
234
|
+
data = arguments.get("data", {})
|
|
235
|
+
|
|
236
|
+
if not node_id:
|
|
237
|
+
return [TextContent(type="text", text="Error: id is required")]
|
|
238
|
+
if node_id in graph["nodes"]:
|
|
239
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' already exists")]
|
|
240
|
+
|
|
241
|
+
# Validate parents exist
|
|
242
|
+
for p in parents:
|
|
243
|
+
if p not in graph["nodes"]:
|
|
244
|
+
return [TextContent(type="text", text=f"Error: parent '{p}' not found")]
|
|
245
|
+
|
|
246
|
+
# Create node
|
|
247
|
+
graph["nodes"][node_id] = {
|
|
248
|
+
"id": node_id,
|
|
249
|
+
"parents": parents,
|
|
250
|
+
"children": [],
|
|
251
|
+
"status": status,
|
|
252
|
+
"notes": [],
|
|
253
|
+
"messages": [],
|
|
254
|
+
"data": data
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# Update parent nodes' children lists
|
|
258
|
+
for p in parents:
|
|
259
|
+
if node_id not in graph["nodes"][p]["children"]:
|
|
260
|
+
graph["nodes"][p]["children"].append(node_id)
|
|
261
|
+
|
|
262
|
+
save_graph(graph)
|
|
263
|
+
return [TextContent(type="text", text=f"Added node '{node_id}'")]
|
|
264
|
+
|
|
265
|
+
elif name == "remove":
|
|
266
|
+
node_id = arguments.get("id", "")
|
|
267
|
+
if node_id == "root":
|
|
268
|
+
return [TextContent(type="text", text="Error: cannot remove root")]
|
|
269
|
+
if node_id not in graph["nodes"]:
|
|
270
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
271
|
+
|
|
272
|
+
node = graph["nodes"][node_id]
|
|
273
|
+
|
|
274
|
+
# Remove from parents' children lists
|
|
275
|
+
for p in node.get("parents", []):
|
|
276
|
+
if p in graph["nodes"]:
|
|
277
|
+
graph["nodes"][p]["children"] = [c for c in graph["nodes"][p]["children"] if c != node_id]
|
|
278
|
+
|
|
279
|
+
# Remove from children's parents lists
|
|
280
|
+
for c in node.get("children", []):
|
|
281
|
+
if c in graph["nodes"]:
|
|
282
|
+
graph["nodes"][c]["parents"] = [p for p in graph["nodes"][c]["parents"] if p != node_id]
|
|
283
|
+
|
|
284
|
+
del graph["nodes"][node_id]
|
|
285
|
+
save_graph(graph)
|
|
286
|
+
return [TextContent(type="text", text=f"Removed node '{node_id}'")]
|
|
287
|
+
|
|
288
|
+
elif name == "get":
|
|
289
|
+
node_id = arguments.get("id", "")
|
|
290
|
+
node = get_node(graph, node_id)
|
|
291
|
+
if not node:
|
|
292
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
293
|
+
return [TextContent(type="text", text=json.dumps(node, indent=2))]
|
|
294
|
+
|
|
295
|
+
elif name == "set_status":
|
|
296
|
+
node_id = arguments.get("id", "")
|
|
297
|
+
status = arguments.get("status", "")
|
|
298
|
+
node = get_node(graph, node_id)
|
|
299
|
+
if not node:
|
|
300
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
301
|
+
node["status"] = status
|
|
302
|
+
save_graph(graph)
|
|
303
|
+
return [TextContent(type="text", text=f"Set status of '{node_id}' to '{status}'")]
|
|
304
|
+
|
|
305
|
+
elif name == "add_data":
|
|
306
|
+
node_id = arguments.get("id", "")
|
|
307
|
+
data = arguments.get("data", {})
|
|
308
|
+
node = get_node(graph, node_id)
|
|
309
|
+
if not node:
|
|
310
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
311
|
+
ensure_node_fields(node)
|
|
312
|
+
node["data"].update(data)
|
|
313
|
+
save_graph(graph)
|
|
314
|
+
return [TextContent(type="text", text=f"Added data to '{node_id}'")]
|
|
315
|
+
|
|
316
|
+
elif name == "add_note":
|
|
317
|
+
node_id = arguments.get("id", "")
|
|
318
|
+
note = arguments.get("note", "")
|
|
319
|
+
node = get_node(graph, node_id)
|
|
320
|
+
if not node:
|
|
321
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
322
|
+
ensure_node_fields(node)
|
|
323
|
+
node["notes"].append(note)
|
|
324
|
+
save_graph(graph)
|
|
325
|
+
return [TextContent(type="text", text=f"Added note to '{node_id}'")]
|
|
326
|
+
|
|
327
|
+
elif name == "send_message":
|
|
328
|
+
node_id = arguments.get("id", "")
|
|
329
|
+
message = arguments.get("message", "")
|
|
330
|
+
from_agent = arguments.get("from_agent", "anonymous")
|
|
331
|
+
node = get_node(graph, node_id)
|
|
332
|
+
if not node:
|
|
333
|
+
return [TextContent(type="text", text=f"Error: node '{node_id}' not found")]
|
|
334
|
+
ensure_node_fields(node)
|
|
335
|
+
node["messages"].append({
|
|
336
|
+
"from": from_agent,
|
|
337
|
+
"text": message,
|
|
338
|
+
"time": datetime.now().isoformat()
|
|
339
|
+
})
|
|
340
|
+
save_graph(graph)
|
|
341
|
+
return [TextContent(type="text", text=f"Message sent to '{node_id}'")]
|
|
342
|
+
|
|
343
|
+
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
344
|
+
|
|
345
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
346
|
+
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def main():
|
|
350
|
+
asyncio.run(run_server())
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
main()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mcp_blackboard_graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mcp_blackboard_graph/main.py,sha256=M9H0LPGTCeuyvYO2TAv3bvEW-dEbNs6_NiN6GdfWkug,12842
|
|
3
|
+
mcp_blackboard_graph-0.1.0.dist-info/METADATA,sha256=yGvEQZHOE6FKeaTqx0iqc0HhCGIifjfgoijI7ofeBYs,221
|
|
4
|
+
mcp_blackboard_graph-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
mcp_blackboard_graph-0.1.0.dist-info/entry_points.txt,sha256=ZO77IWenT2hhOo7qZWe5pY4a39S-agBtDSvMKScaHKg,72
|
|
6
|
+
mcp_blackboard_graph-0.1.0.dist-info/top_level.txt,sha256=LjEpQyjyDJ_NyaSz3sfAY9q-tlDsIb1c7oNBj6CVKQI,21
|
|
7
|
+
mcp_blackboard_graph-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp_blackboard_graph
|