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.
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,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-blackboard-graph
3
+ Version: 0.1.0
4
+ Summary: MCP server for multi-agent coordination via shared graph - nodes with messages, notes, and status
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: mcp>=1.0.0
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-blackboard-graph = mcp_blackboard_graph.main:main
@@ -0,0 +1 @@
1
+ mcp_blackboard_graph