guohanghui commited on
Commit
d98e8e2
·
verified ·
1 Parent(s): 0b64e95

Update langgraph/mcp_output/mcp_plugin/mcp_service.py

Browse files
langgraph/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,87 +1,443 @@
1
- from fastmcp import FastMCP, subprocess, ctypes
2
-
3
- class CppServiceWrapper:
4
- def __init__(self, executable_path):
5
- self.executable_path = executable_path
6
-
7
- def call_executable(self, *args):
8
- try:
9
- result = subprocess.run([self.executable_path] + list(args), capture_output=True, text=True)
10
- if result.returncode != 0:
11
- return {"success": False, "error": result.stderr}
12
- return {"success": True, "result": result.stdout}
13
- except Exception as e:
14
- return {"success": False, "error": str(e)}
15
-
16
- def load_dynamic_library(self, lib_path):
17
- try:
18
- return ctypes.CDLL(lib_path)
19
- except OSError as e:
20
- return None
21
-
22
- def compile_project(build_system, source_dir):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  try:
24
- if build_system == "cmake":
25
- subprocess.run(["cmake", source_dir], check=True)
26
- subprocess.run(["make"], check=True)
27
- elif build_system == "make":
28
- subprocess.run(["make", "-C", source_dir], check=True)
29
- elif build_system == "configure":
30
- subprocess.run(["./configure"], cwd=source_dir, check=True)
31
- subprocess.run(["make"], cwd=source_dir, check=True)
 
 
 
 
 
 
 
 
32
  else:
33
- return {"success": False, "error": "Unsupported build system"}
34
- return {"success": True}
35
- except subprocess.CalledProcessError as e:
36
- return {"success": False, "error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- mcp = FastMCP("cpp_service")
39
 
40
- @ mcp.tool(name="compile_project", description="Compile the C/C++ project using the specified build system.")
41
- def compile_project_tool(build_system: str, source_dir: str) -> dict:
 
 
 
 
42
  """
43
- Compile the C/C++ project using the specified build system.
44
 
45
- Parameters:
46
- - build_system (str): The build system to use (e.g., 'cmake', 'make', 'configure').
47
- - source_dir (str): The directory containing the source code.
 
48
 
49
  Returns:
50
- - dict: A dictionary containing 'success' and 'error' fields.
51
  """
52
- return compile_project(build_system, source_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- @ mcp.tool(name="execute_cpp_function", description="Execute a C++ function via the compiled executable.")
55
- def execute_cpp_function(executable_path: str, *args: str) -> dict:
 
 
 
 
56
  """
57
- Execute a C++ function via the compiled executable.
58
 
59
- Parameters:
60
- - executable_path (str): Path to the compiled executable.
61
- - args (str): Arguments to pass to the executable.
 
62
 
63
  Returns:
64
- - dict: A dictionary containing 'success', 'result', and 'error' fields.
65
  """
66
- wrapper = CppServiceWrapper(executable_path)
67
- return wrapper.call_executable(*args)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- @ mcp.tool(name="load_cpp_library", description="Load a C++ dynamic library and return its handle.")
70
- def load_cpp_library(lib_path: str) -> dict:
 
 
 
 
71
  """
72
- Load a C++ dynamic library and return its handle.
73
 
74
- Parameters:
75
- - lib_path (str): Path to the dynamic library.
 
 
76
 
77
  Returns:
78
- - dict: A dictionary containing 'success' and 'result' fields.
79
  """
80
- wrapper = CppServiceWrapper("")
81
- lib = wrapper.load_dynamic_library(lib_path)
82
- if lib is None:
83
- return {"success": False, "error": "Failed to load library"}
84
- return {"success": True, "result": "Library loaded successfully"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- def create_app():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  return mcp
 
1
+ import os
2
+ import sys
3
+ from typing import Dict, Any, List, Optional, Union
4
+ import json
5
+
6
+ # Add the local source directory to sys.path
7
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
8
+ if source_path not in sys.path:
9
+ sys.path.insert(0, source_path)
10
+
11
+ from fastmcp import FastMCP
12
+
13
+ # Import LangGraph modules
14
+ try:
15
+ from langgraph.graph import StateGraph, Graph, END
16
+ from langgraph.prebuilt import ToolExecutor, ToolInvocation
17
+ from typing_extensions import TypedDict
18
+ except ImportError:
19
+ # Fallback for basic functionality
20
+ StateGraph = None
21
+ Graph = None
22
+ END = "__end__"
23
+
24
+ # Create the FastMCP service application
25
+ mcp = FastMCP("langgraph_service")
26
+
27
+ # Storage for graphs
28
+ _graphs: Dict[str, Any] = {}
29
+ _compiled_graphs: Dict[str, Any] = {}
30
+
31
+
32
+ @mcp.tool(name="create_state_graph", description="Create a new StateGraph")
33
+ def create_state_graph(
34
+ graph_id: str,
35
+ state_schema: Optional[Dict[str, str]] = None
36
+ ) -> Dict[str, Any]:
37
+ """
38
+ Create a new StateGraph for building stateful workflows.
39
+
40
+ Args:
41
+ graph_id: Unique identifier for the graph
42
+ state_schema: Optional schema defining state structure (dict of field_name: type_name)
43
+
44
+ Returns:
45
+ Dictionary with success status and graph info
46
+ """
47
  try:
48
+ if StateGraph is None:
49
+ return {"success": False, "result": None, "error": "StateGraph not available"}
50
+
51
+ # Create a simple state type
52
+ if state_schema:
53
+ # Create a TypedDict-like state class dynamically
54
+ state_fields = {k: str for k, v in state_schema.items()}
55
+
56
+ class GraphState(TypedDict):
57
+ pass
58
+
59
+ # Add fields
60
+ for field, ftype in state_fields.items():
61
+ GraphState.__annotations__[field] = Any
62
+
63
+ graph = StateGraph(GraphState)
64
  else:
65
+ # Default state with messages
66
+ class DefaultState(TypedDict):
67
+ messages: List[str]
68
+
69
+ graph = StateGraph(DefaultState)
70
+
71
+ _graphs[graph_id] = {
72
+ "graph": graph,
73
+ "nodes": [],
74
+ "edges": [],
75
+ "compiled": False
76
+ }
77
+
78
+ return {
79
+ "success": True,
80
+ "result": {
81
+ "graph_id": graph_id,
82
+ "state_schema": state_schema or {"messages": "List[str]"}
83
+ },
84
+ "error": None
85
+ }
86
+ except Exception as e:
87
+ return {"success": False, "result": None, "error": str(e)}
88
 
 
89
 
90
+ @mcp.tool(name="add_node", description="Add a node to a graph")
91
+ def add_node(
92
+ graph_id: str,
93
+ node_name: str,
94
+ node_type: str = "function"
95
+ ) -> Dict[str, Any]:
96
  """
97
+ Add a node to a StateGraph.
98
 
99
+ Args:
100
+ graph_id: ID of the graph
101
+ node_name: Name of the node
102
+ node_type: Type of node (function, tool, etc.)
103
 
104
  Returns:
105
+ Dictionary with success status
106
  """
107
+ try:
108
+ if graph_id not in _graphs:
109
+ return {"success": False, "result": None, "error": f"Graph '{graph_id}' not found"}
110
+
111
+ graph_data = _graphs[graph_id]
112
+
113
+ if graph_data["compiled"]:
114
+ return {"success": False, "result": None, "error": "Cannot modify compiled graph"}
115
+
116
+ # Create a simple node function
117
+ def node_function(state):
118
+ # Default: pass through state with a message
119
+ if "messages" in state:
120
+ messages = state.get("messages", [])
121
+ messages.append(f"Processed by {node_name}")
122
+ return {"messages": messages}
123
+ return state
124
+
125
+ graph_data["graph"].add_node(node_name, node_function)
126
+ graph_data["nodes"].append(node_name)
127
+
128
+ return {
129
+ "success": True,
130
+ "result": {
131
+ "graph_id": graph_id,
132
+ "node_name": node_name,
133
+ "node_type": node_type
134
+ },
135
+ "error": None
136
+ }
137
+ except Exception as e:
138
+ return {"success": False, "result": None, "error": str(e)}
139
+
140
 
141
+ @mcp.tool(name="add_edge", description="Add an edge between two nodes")
142
+ def add_edge(
143
+ graph_id: str,
144
+ from_node: str,
145
+ to_node: str
146
+ ) -> Dict[str, Any]:
147
  """
148
+ Add an edge connecting two nodes in a graph.
149
 
150
+ Args:
151
+ graph_id: ID of the graph
152
+ from_node: Source node name
153
+ to_node: Target node name (use "__end__" for terminal node)
154
 
155
  Returns:
156
+ Dictionary with success status
157
  """
158
+ try:
159
+ if graph_id not in _graphs:
160
+ return {"success": False, "result": None, "error": f"Graph '{graph_id}' not found"}
161
+
162
+ graph_data = _graphs[graph_id]
163
+
164
+ if graph_data["compiled"]:
165
+ return {"success": False, "result": None, "error": "Cannot modify compiled graph"}
166
+
167
+ graph_data["graph"].add_edge(from_node, to_node)
168
+ graph_data["edges"].append({"from": from_node, "to": to_node})
169
+
170
+ return {
171
+ "success": True,
172
+ "result": {
173
+ "graph_id": graph_id,
174
+ "from_node": from_node,
175
+ "to_node": to_node
176
+ },
177
+ "error": None
178
+ }
179
+ except Exception as e:
180
+ return {"success": False, "result": None, "error": str(e)}
181
+
182
 
183
+ @mcp.tool(name="add_conditional_edge", description="Add a conditional edge with routing logic")
184
+ def add_conditional_edge(
185
+ graph_id: str,
186
+ from_node: str,
187
+ condition_type: str = "simple"
188
+ ) -> Dict[str, Any]:
189
  """
190
+ Add a conditional edge that routes based on state.
191
 
192
+ Args:
193
+ graph_id: ID of the graph
194
+ from_node: Source node name
195
+ condition_type: Type of condition (simple, multi, etc.)
196
 
197
  Returns:
198
+ Dictionary with success status
199
  """
200
+ try:
201
+ if graph_id not in _graphs:
202
+ return {"success": False, "result": None, "error": f"Graph '{graph_id}' not found"}
203
+
204
+ graph_data = _graphs[graph_id]
205
+
206
+ if graph_data["compiled"]:
207
+ return {"success": False, "result": None, "error": "Cannot modify compiled graph"}
208
+
209
+ # Simple routing function
210
+ def route_function(state):
211
+ # Default routing based on message count
212
+ messages = state.get("messages", [])
213
+ if len(messages) > 3:
214
+ return END
215
+ else:
216
+ # Route to first available node or END
217
+ nodes = graph_data["nodes"]
218
+ return nodes[0] if nodes else END
219
+
220
+ graph_data["graph"].add_conditional_edges(
221
+ from_node,
222
+ route_function
223
+ )
224
+
225
+ return {
226
+ "success": True,
227
+ "result": {
228
+ "graph_id": graph_id,
229
+ "from_node": from_node,
230
+ "condition_type": condition_type
231
+ },
232
+ "error": None
233
+ }
234
+ except Exception as e:
235
+ return {"success": False, "result": None, "error": str(e)}
236
+
237
 
238
+ @mcp.tool(name="set_entry_point", description="Set the entry point of the graph")
239
+ def set_entry_point(
240
+ graph_id: str,
241
+ node_name: str
242
+ ) -> Dict[str, Any]:
243
+ """
244
+ Set the entry point (starting node) for a graph.
245
+
246
+ Args:
247
+ graph_id: ID of the graph
248
+ node_name: Name of the entry node
249
+
250
+ Returns:
251
+ Dictionary with success status
252
+ """
253
+ try:
254
+ if graph_id not in _graphs:
255
+ return {"success": False, "result": None, "error": f"Graph '{graph_id}' not found"}
256
+
257
+ graph_data = _graphs[graph_id]
258
+
259
+ if graph_data["compiled"]:
260
+ return {"success": False, "result": None, "error": "Cannot modify compiled graph"}
261
+
262
+ graph_data["graph"].set_entry_point(node_name)
263
+
264
+ return {
265
+ "success": True,
266
+ "result": {
267
+ "graph_id": graph_id,
268
+ "entry_point": node_name
269
+ },
270
+ "error": None
271
+ }
272
+ except Exception as e:
273
+ return {"success": False, "result": None, "error": str(e)}
274
+
275
+
276
+ @mcp.tool(name="compile_graph", description="Compile a graph for execution")
277
+ def compile_graph(
278
+ graph_id: str
279
+ ) -> Dict[str, Any]:
280
+ """
281
+ Compile a StateGraph into an executable workflow.
282
+
283
+ Args:
284
+ graph_id: ID of the graph to compile
285
+
286
+ Returns:
287
+ Dictionary with success status and compiled graph info
288
+ """
289
+ try:
290
+ if graph_id not in _graphs:
291
+ return {"success": False, "result": None, "error": f"Graph '{graph_id}' not found"}
292
+
293
+ graph_data = _graphs[graph_id]
294
+
295
+ if graph_data["compiled"]:
296
+ return {"success": False, "result": None, "error": "Graph already compiled"}
297
+
298
+ compiled = graph_data["graph"].compile()
299
+ _compiled_graphs[graph_id] = compiled
300
+ graph_data["compiled"] = True
301
+
302
+ return {
303
+ "success": True,
304
+ "result": {
305
+ "graph_id": graph_id,
306
+ "num_nodes": len(graph_data["nodes"]),
307
+ "num_edges": len(graph_data["edges"]),
308
+ "compiled": True
309
+ },
310
+ "error": None
311
+ }
312
+ except Exception as e:
313
+ return {"success": False, "result": None, "error": str(e)}
314
+
315
+
316
+ @mcp.tool(name="invoke_graph", description="Execute a compiled graph with input")
317
+ def invoke_graph(
318
+ graph_id: str,
319
+ input_data: Dict[str, Any]
320
+ ) -> Dict[str, Any]:
321
+ """
322
+ Execute a compiled graph with given input.
323
+
324
+ Args:
325
+ graph_id: ID of the compiled graph
326
+ input_data: Input state/data for the graph
327
+
328
+ Returns:
329
+ Dictionary with execution result
330
+ """
331
+ try:
332
+ if graph_id not in _compiled_graphs:
333
+ return {"success": False, "result": None, "error": f"Compiled graph '{graph_id}' not found"}
334
+
335
+ compiled_graph = _compiled_graphs[graph_id]
336
+ result = compiled_graph.invoke(input_data)
337
+
338
+ return {
339
+ "success": True,
340
+ "result": {
341
+ "graph_id": graph_id,
342
+ "output": result
343
+ },
344
+ "error": None
345
+ }
346
+ except Exception as e:
347
+ return {"success": False, "result": None, "error": str(e)}
348
+
349
+
350
+ @mcp.tool(name="get_graph_info", description="Get information about a graph")
351
+ def get_graph_info(graph_id: str) -> Dict[str, Any]:
352
+ """
353
+ Get detailed information about a graph.
354
+
355
+ Args:
356
+ graph_id: ID of the graph
357
+
358
+ Returns:
359
+ Dictionary with graph details
360
+ """
361
+ try:
362
+ if graph_id not in _graphs:
363
+ return {"success": False, "result": None, "error": f"Graph '{graph_id}' not found"}
364
+
365
+ graph_data = _graphs[graph_id]
366
+
367
+ return {
368
+ "success": True,
369
+ "result": {
370
+ "graph_id": graph_id,
371
+ "nodes": graph_data["nodes"],
372
+ "edges": graph_data["edges"],
373
+ "compiled": graph_data["compiled"]
374
+ },
375
+ "error": None
376
+ }
377
+ except Exception as e:
378
+ return {"success": False, "result": None, "error": str(e)}
379
+
380
+
381
+ @mcp.tool(name="list_graphs", description="List all stored graphs")
382
+ def list_graphs() -> Dict[str, Any]:
383
+ """
384
+ List all stored graphs.
385
+
386
+ Returns:
387
+ Dictionary with list of graph IDs
388
+ """
389
+ try:
390
+ graphs_info = []
391
+ for gid, gdata in _graphs.items():
392
+ graphs_info.append({
393
+ "graph_id": gid,
394
+ "num_nodes": len(gdata["nodes"]),
395
+ "num_edges": len(gdata["edges"]),
396
+ "compiled": gdata["compiled"]
397
+ })
398
+
399
+ return {
400
+ "success": True,
401
+ "result": {"graphs": graphs_info},
402
+ "error": None
403
+ }
404
+ except Exception as e:
405
+ return {"success": False, "result": None, "error": str(e)}
406
+
407
+
408
+ @mcp.tool(name="delete_graph", description="Delete a graph")
409
+ def delete_graph(graph_id: str) -> Dict[str, Any]:
410
+ """
411
+ Delete a stored graph.
412
+
413
+ Args:
414
+ graph_id: ID of the graph to delete
415
+
416
+ Returns:
417
+ Dictionary with success status
418
+ """
419
+ try:
420
+ if graph_id not in _graphs:
421
+ return {"success": False, "result": None, "error": f"Graph '{graph_id}' not found"}
422
+
423
+ del _graphs[graph_id]
424
+ if graph_id in _compiled_graphs:
425
+ del _compiled_graphs[graph_id]
426
+
427
+ return {
428
+ "success": True,
429
+ "result": {"deleted": graph_id},
430
+ "error": None
431
+ }
432
+ except Exception as e:
433
+ return {"success": False, "result": None, "error": str(e)}
434
+
435
+
436
+ def create_app() -> FastMCP:
437
+ """
438
+ Create and return the FastMCP application instance.
439
+
440
+ Returns:
441
+ FastMCP: The FastMCP application instance.
442
+ """
443
  return mcp