agent-mcp 0.1.2__py3-none-any.whl → 0.1.3__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.
- agent_mcp/__init__.py +16 -0
- agent_mcp/crewai_mcp_adapter.py +281 -0
- agent_mcp/enhanced_mcp_agent.py +601 -0
- agent_mcp/heterogeneous_group_chat.py +424 -0
- agent_mcp/langchain_mcp_adapter.py +325 -0
- agent_mcp/langgraph_mcp_adapter.py +325 -0
- agent_mcp/mcp_agent.py +632 -0
- agent_mcp/mcp_decorator.py +257 -0
- agent_mcp/mcp_langgraph.py +733 -0
- agent_mcp/mcp_transaction.py +97 -0
- agent_mcp/mcp_transport.py +700 -0
- agent_mcp/mcp_transport_enhanced.py +46 -0
- agent_mcp/proxy_agent.py +24 -0
- agent_mcp-0.1.3.dist-info/METADATA +331 -0
- agent_mcp-0.1.3.dist-info/RECORD +18 -0
- agent_mcp-0.1.3.dist-info/top_level.txt +1 -0
- agent_mcp-0.1.2.dist-info/METADATA +0 -475
- agent_mcp-0.1.2.dist-info/RECORD +0 -5
- agent_mcp-0.1.2.dist-info/top_level.txt +0 -1
- {agent_mcp-0.1.2.dist-info → agent_mcp-0.1.3.dist-info}/WHEEL +0 -0
- {agent_mcp-0.1.2.dist-info → agent_mcp-0.1.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCPLangGraph - A LangGraph node with Model Context Protocol capabilities.
|
|
3
|
+
|
|
4
|
+
This module provides a transparent implementation of the Model Context Protocol
|
|
5
|
+
for LangGraph, allowing nodes to standardize context provision to LLMs and
|
|
6
|
+
interact with other MCP-capable systems with minimal configuration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import uuid
|
|
11
|
+
import inspect
|
|
12
|
+
from typing import Any, Dict, List, Optional, Union, Callable, TypeVar, Type, cast
|
|
13
|
+
|
|
14
|
+
# Import LangGraph components
|
|
15
|
+
import langgraph.graph
|
|
16
|
+
from langgraph.graph import END, StateGraph
|
|
17
|
+
from langgraph.prebuilt import create_react_agent, ToolNode
|
|
18
|
+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
|
19
|
+
from langchain_core.runnables import RunnableConfig
|
|
20
|
+
from langchain_core.tools import tool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
T = TypeVar('T')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SharedContext:
|
|
27
|
+
"""
|
|
28
|
+
A shared context store that can be used by multiple MCPNodes.
|
|
29
|
+
|
|
30
|
+
This class provides a centralized context store that allows multiple
|
|
31
|
+
MCPNodes to share context with each other, enabling seamless
|
|
32
|
+
context sharing across a LangGraph agent network.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
context_store (Dict): The shared context store
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
"""Initialize a new shared context store."""
|
|
40
|
+
self.context_store = {}
|
|
41
|
+
|
|
42
|
+
def set(self, key: str, value: Any) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Set a value in the shared context.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
key: The key to store the value under
|
|
48
|
+
value: The value to store
|
|
49
|
+
"""
|
|
50
|
+
self.context_store[key] = value
|
|
51
|
+
|
|
52
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
53
|
+
"""
|
|
54
|
+
Get a value from the shared context.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
key: The key to retrieve
|
|
58
|
+
default: Default value to return if key not found
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The value associated with the key, or the default if not found
|
|
62
|
+
"""
|
|
63
|
+
return self.context_store.get(key, default)
|
|
64
|
+
|
|
65
|
+
def has(self, key: str) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Check if a key exists in the shared context.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
key: The key to check for
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if the key exists, False otherwise
|
|
74
|
+
"""
|
|
75
|
+
return key in self.context_store
|
|
76
|
+
|
|
77
|
+
def remove(self, key: str) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
Remove a key from the shared context.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
key: The key to remove
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if the key was removed, False if it didn't exist
|
|
86
|
+
"""
|
|
87
|
+
if key in self.context_store:
|
|
88
|
+
del self.context_store[key]
|
|
89
|
+
return True
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
def list_keys(self) -> List[str]:
|
|
93
|
+
"""
|
|
94
|
+
List all keys in the shared context.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of all keys in the context
|
|
98
|
+
"""
|
|
99
|
+
return list(self.context_store.keys())
|
|
100
|
+
|
|
101
|
+
def clear(self) -> None:
|
|
102
|
+
"""Clear all keys from the shared context."""
|
|
103
|
+
self.context_store.clear()
|
|
104
|
+
|
|
105
|
+
def update(self, other_context: Dict[str, Any]) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Update the shared context with another dictionary.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
other_context: Dictionary to update the context with
|
|
111
|
+
"""
|
|
112
|
+
self.context_store.update(other_context)
|
|
113
|
+
|
|
114
|
+
class MCPNode:
|
|
115
|
+
"""A LangGraph node with Model Context Protocol capabilities.
|
|
116
|
+
|
|
117
|
+
This class provides a standardized implementation of the Model Context Protocol
|
|
118
|
+
for LangGraph nodes, enabling seamless context sharing between different parts
|
|
119
|
+
of agent graphs. It supports both local and shared context management, allowing
|
|
120
|
+
nodes to either maintain their own context or participate in a shared context
|
|
121
|
+
environment.
|
|
122
|
+
|
|
123
|
+
Features:
|
|
124
|
+
- Context Management: Both local and shared context support
|
|
125
|
+
- Tool Integration: Register and manage MCP-compatible tools
|
|
126
|
+
- LLM Integration: Seamless integration with language models
|
|
127
|
+
- Context Sharing: Share context between nodes in a graph
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> shared_context = SharedContext()
|
|
131
|
+
>>> node = MCPNode("my_node", context=shared_context)
|
|
132
|
+
>>> node.update_context("key", "value")
|
|
133
|
+
>>> value = node.get_context("key")
|
|
134
|
+
|
|
135
|
+
Attributes:
|
|
136
|
+
name (str): Name of the node
|
|
137
|
+
llm (Any): Language model instance for this node
|
|
138
|
+
mcp_tools (Dict): Registry of MCP tools available to this node
|
|
139
|
+
mcp_id (str): Unique identifier for this MCP node
|
|
140
|
+
mcp_version (str): The MCP version implemented by this node
|
|
141
|
+
_shared_context (SharedContext): Optional shared context instance
|
|
142
|
+
_use_shared_context (bool): Whether using shared or local context
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
name: str,
|
|
148
|
+
system_message: Optional[str] = None,
|
|
149
|
+
context: Optional[SharedContext] = None,
|
|
150
|
+
llm: Any = None,
|
|
151
|
+
**kwargs
|
|
152
|
+
):
|
|
153
|
+
"""
|
|
154
|
+
Initialize an MCPNode.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
name: The name of the node
|
|
158
|
+
system_message: Optional system message to include in the node's context
|
|
159
|
+
context: Optional shared context object to use instead of local context
|
|
160
|
+
llm: The language model to use with this node
|
|
161
|
+
**kwargs: Additional keyword arguments
|
|
162
|
+
"""
|
|
163
|
+
self.name = name
|
|
164
|
+
self.llm = llm
|
|
165
|
+
|
|
166
|
+
# MCP specific attributes
|
|
167
|
+
self.mcp_tools = {}
|
|
168
|
+
self.mcp_id = str(uuid.uuid4())
|
|
169
|
+
self.mcp_version = "0.1.0" # MCP version implemented
|
|
170
|
+
|
|
171
|
+
# Set up context - either use provided shared context or create local context
|
|
172
|
+
if context is not None and isinstance(context, SharedContext):
|
|
173
|
+
# Use provided shared context
|
|
174
|
+
self._shared_context = context
|
|
175
|
+
self._use_shared_context = True
|
|
176
|
+
# Set a node-specific key in the shared context to store node-specific data
|
|
177
|
+
node_key = f"node_{self.mcp_id}"
|
|
178
|
+
if not self._shared_context.has(node_key):
|
|
179
|
+
self._shared_context.set(node_key, {})
|
|
180
|
+
else:
|
|
181
|
+
# Use local context
|
|
182
|
+
self.context_store = {}
|
|
183
|
+
self._use_shared_context = False
|
|
184
|
+
|
|
185
|
+
# Add system message to context if provided
|
|
186
|
+
if system_message:
|
|
187
|
+
self.update_context("system_message", system_message)
|
|
188
|
+
|
|
189
|
+
# Register default MCP tools
|
|
190
|
+
self._register_default_mcp_tools()
|
|
191
|
+
|
|
192
|
+
def _register_default_mcp_tools(self):
|
|
193
|
+
"""Register default MCP tools that are available to all MCP nodes."""
|
|
194
|
+
|
|
195
|
+
# Define tool functions as simple Python functions
|
|
196
|
+
def context_get(key: str) -> Dict:
|
|
197
|
+
"""Get a context item by key."""
|
|
198
|
+
return self._mcp_context_get(key)
|
|
199
|
+
|
|
200
|
+
def context_set(key: str, value: str) -> Dict:
|
|
201
|
+
"""Set a context item with the given key and value."""
|
|
202
|
+
return self._mcp_context_set(key, value)
|
|
203
|
+
|
|
204
|
+
def context_list() -> Dict:
|
|
205
|
+
"""List all available context keys."""
|
|
206
|
+
return self._mcp_context_list()
|
|
207
|
+
|
|
208
|
+
def context_remove(key: str) -> Dict:
|
|
209
|
+
"""Remove a context item by key."""
|
|
210
|
+
return self._mcp_context_remove(key)
|
|
211
|
+
|
|
212
|
+
def mcp_info() -> Dict:
|
|
213
|
+
"""Get information about this MCP node's capabilities."""
|
|
214
|
+
return self._mcp_info()
|
|
215
|
+
|
|
216
|
+
# Register the tools using our custom method for better compatibility
|
|
217
|
+
self.register_custom_tool("context_get", "Get a context item by key", context_get)
|
|
218
|
+
self.register_custom_tool("context_set", "Set a context item with the given key and value", context_set)
|
|
219
|
+
self.register_custom_tool("context_list", "List all available context keys", context_list)
|
|
220
|
+
self.register_custom_tool("context_remove", "Remove a context item by key", context_remove)
|
|
221
|
+
self.register_custom_tool("mcp_info", "Get information about this MCP node's capabilities", mcp_info)
|
|
222
|
+
|
|
223
|
+
def register_mcp_tool(self, tool_func: Callable) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Register an MCP tool with this node.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
tool_func: A LangChain tool function to register
|
|
229
|
+
"""
|
|
230
|
+
# Extract information from the tool decorator
|
|
231
|
+
if hasattr(tool_func, "name"):
|
|
232
|
+
tool_name = tool_func.name
|
|
233
|
+
elif hasattr(tool_func, "__name__"):
|
|
234
|
+
tool_name = tool_func.__name__
|
|
235
|
+
else:
|
|
236
|
+
# Generate a unique name if no name attribute exists
|
|
237
|
+
tool_name = f"tool_{str(uuid.uuid4())[:8]}"
|
|
238
|
+
|
|
239
|
+
# Get tool description
|
|
240
|
+
if hasattr(tool_func, "description"):
|
|
241
|
+
tool_description = tool_func.description
|
|
242
|
+
else:
|
|
243
|
+
tool_description = tool_func.__doc__ if hasattr(tool_func, "__doc__") and tool_func.__doc__ else "No description provided"
|
|
244
|
+
|
|
245
|
+
# Inspect function signature to build parameter info
|
|
246
|
+
try:
|
|
247
|
+
sig = inspect.signature(tool_func)
|
|
248
|
+
params = []
|
|
249
|
+
|
|
250
|
+
for param_name, param in sig.parameters.items():
|
|
251
|
+
if param_name == 'self':
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
param_info = {
|
|
255
|
+
"name": param_name,
|
|
256
|
+
"description": f"Parameter {param_name}",
|
|
257
|
+
"required": param.default == inspect.Parameter.empty
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Add type information if available
|
|
261
|
+
if param.annotation != inspect.Parameter.empty:
|
|
262
|
+
try:
|
|
263
|
+
if hasattr(param.annotation, "__name__"):
|
|
264
|
+
type_name = param.annotation.__name__
|
|
265
|
+
if type_name in ["str", "string"]:
|
|
266
|
+
param_info["type"] = "string"
|
|
267
|
+
elif type_name in ["int", "integer", "float", "number"]:
|
|
268
|
+
param_info["type"] = "number"
|
|
269
|
+
elif type_name in ["bool", "boolean"]:
|
|
270
|
+
param_info["type"] = "boolean"
|
|
271
|
+
else:
|
|
272
|
+
param_info["type"] = "string" # Default to string for other types
|
|
273
|
+
else:
|
|
274
|
+
param_info["type"] = "string" # Default to string for complex types
|
|
275
|
+
except Exception:
|
|
276
|
+
# If we can't get the type, use string as default for Gemini
|
|
277
|
+
param_info["type"] = "string"
|
|
278
|
+
else:
|
|
279
|
+
# If no annotation, add default type for Gemini compatibility
|
|
280
|
+
param_info["type"] = "string"
|
|
281
|
+
|
|
282
|
+
params.append(param_info)
|
|
283
|
+
except (ValueError, TypeError):
|
|
284
|
+
# If we can't inspect the signature, use an empty parameter list
|
|
285
|
+
params = []
|
|
286
|
+
|
|
287
|
+
# Register the tool
|
|
288
|
+
self.mcp_tools[tool_name] = {
|
|
289
|
+
"name": tool_name,
|
|
290
|
+
"description": tool_description,
|
|
291
|
+
"parameters": params,
|
|
292
|
+
"function": tool_func,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
def register_custom_tool(
|
|
296
|
+
self,
|
|
297
|
+
name: str,
|
|
298
|
+
description: str,
|
|
299
|
+
func: Callable,
|
|
300
|
+
**kwargs
|
|
301
|
+
) -> None:
|
|
302
|
+
"""
|
|
303
|
+
Register a custom function as an MCP tool.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
name: The name of the tool
|
|
307
|
+
description: Description of the tool
|
|
308
|
+
func: The function to be called
|
|
309
|
+
**kwargs: Additional parameters
|
|
310
|
+
"""
|
|
311
|
+
# Instead of using the tool decorator which may vary between versions,
|
|
312
|
+
# directly register the function with our metadata
|
|
313
|
+
|
|
314
|
+
# Inspect function signature to build parameter info
|
|
315
|
+
params = []
|
|
316
|
+
try:
|
|
317
|
+
sig = inspect.signature(func)
|
|
318
|
+
|
|
319
|
+
for param_name, param in sig.parameters.items():
|
|
320
|
+
if param_name == 'self':
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
param_info = {
|
|
324
|
+
"name": param_name,
|
|
325
|
+
"description": f"Parameter {param_name}",
|
|
326
|
+
"required": param.default == inspect.Parameter.empty,
|
|
327
|
+
"type": "string" # Set a default type for Gemini compatibility
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Add more specific type information if available
|
|
331
|
+
if param.annotation != inspect.Parameter.empty:
|
|
332
|
+
try:
|
|
333
|
+
if hasattr(param.annotation, "__name__"):
|
|
334
|
+
type_name = param.annotation.__name__
|
|
335
|
+
if type_name in ["str", "string"]:
|
|
336
|
+
param_info["type"] = "string"
|
|
337
|
+
elif type_name in ["int", "integer", "float", "number"]:
|
|
338
|
+
param_info["type"] = "number"
|
|
339
|
+
elif type_name in ["bool", "boolean"]:
|
|
340
|
+
param_info["type"] = "boolean"
|
|
341
|
+
else:
|
|
342
|
+
param_info["type"] = "string" # Default to string for other types
|
|
343
|
+
else:
|
|
344
|
+
param_info["type"] = "string" # Default to string for complex types
|
|
345
|
+
except Exception:
|
|
346
|
+
# If we can't get the type, use string as default for Gemini
|
|
347
|
+
param_info["type"] = "string"
|
|
348
|
+
|
|
349
|
+
params.append(param_info)
|
|
350
|
+
except (ValueError, TypeError):
|
|
351
|
+
# If we can't inspect the signature, use an empty parameter list
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
self.mcp_tools[name] = {
|
|
355
|
+
"name": name,
|
|
356
|
+
"description": description,
|
|
357
|
+
"parameters": params,
|
|
358
|
+
"function": func,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
def get_tools_for_node(self) -> List:
|
|
362
|
+
"""
|
|
363
|
+
Get all MCP tools formatted for use in a LangGraph node.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
List of LangChain tool objects
|
|
367
|
+
"""
|
|
368
|
+
return [
|
|
369
|
+
tool_info["function"]
|
|
370
|
+
for tool_info in self.mcp_tools.values()
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
# MCP Context Tool Implementations
|
|
374
|
+
def _mcp_context_get(self, key: str) -> Dict:
|
|
375
|
+
"""
|
|
376
|
+
Get a context item by key.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
key: The key of the context item to retrieve
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Dict containing the value or an error message
|
|
383
|
+
"""
|
|
384
|
+
if self._use_shared_context:
|
|
385
|
+
if self._shared_context.has(key):
|
|
386
|
+
return {"status": "success", "value": self._shared_context.get(key)}
|
|
387
|
+
return {"status": "error", "message": f"Key '{key}' not found in shared context"}
|
|
388
|
+
else:
|
|
389
|
+
if key in self.context_store:
|
|
390
|
+
return {"status": "success", "value": self.context_store[key]}
|
|
391
|
+
return {"status": "error", "message": f"Key '{key}' not found in context"}
|
|
392
|
+
|
|
393
|
+
def _mcp_context_set(self, key: str, value: Any) -> Dict:
|
|
394
|
+
"""
|
|
395
|
+
Set a context item with the given key and value.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
key: The key to store the value under
|
|
399
|
+
value: The value to store
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Dict indicating success or failure
|
|
403
|
+
"""
|
|
404
|
+
if self._use_shared_context:
|
|
405
|
+
self._shared_context.set(key, value)
|
|
406
|
+
return {"status": "success", "message": f"Shared context key '{key}' set successfully"}
|
|
407
|
+
else:
|
|
408
|
+
self.context_store[key] = value
|
|
409
|
+
return {"status": "success", "message": f"Context key '{key}' set successfully"}
|
|
410
|
+
|
|
411
|
+
def _mcp_context_list(self) -> Dict:
|
|
412
|
+
"""
|
|
413
|
+
List all available context keys.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Dict containing the list of context keys
|
|
417
|
+
"""
|
|
418
|
+
if self._use_shared_context:
|
|
419
|
+
return {"status": "success", "keys": self._shared_context.list_keys()}
|
|
420
|
+
else:
|
|
421
|
+
return {"status": "success", "keys": list(self.context_store.keys())}
|
|
422
|
+
|
|
423
|
+
def _mcp_context_remove(self, key: str) -> Dict:
|
|
424
|
+
"""
|
|
425
|
+
Remove a context item by key.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
key: The key of the context item to remove
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Dict indicating success or failure
|
|
432
|
+
"""
|
|
433
|
+
if self._use_shared_context:
|
|
434
|
+
if self._shared_context.has(key):
|
|
435
|
+
self._shared_context.remove(key)
|
|
436
|
+
return {"status": "success", "message": f"Shared context key '{key}' removed successfully"}
|
|
437
|
+
return {"status": "error", "message": f"Key '{key}' not found in shared context"}
|
|
438
|
+
else:
|
|
439
|
+
if key in self.context_store:
|
|
440
|
+
del self.context_store[key]
|
|
441
|
+
return {"status": "success", "message": f"Context key '{key}' removed successfully"}
|
|
442
|
+
return {"status": "error", "message": f"Key '{key}' not found in context"}
|
|
443
|
+
|
|
444
|
+
def _mcp_info(self) -> Dict:
|
|
445
|
+
"""
|
|
446
|
+
Get information about this MCP node's capabilities.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Dict containing MCP node information
|
|
450
|
+
"""
|
|
451
|
+
return {
|
|
452
|
+
"id": self.mcp_id,
|
|
453
|
+
"name": self.name,
|
|
454
|
+
"version": self.mcp_version,
|
|
455
|
+
"tools": [
|
|
456
|
+
{
|
|
457
|
+
"name": name,
|
|
458
|
+
"description": tool["description"],
|
|
459
|
+
"parameters": tool["parameters"]
|
|
460
|
+
}
|
|
461
|
+
for name, tool in self.mcp_tools.items()
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
def update_context(self, key: str, value: Any) -> None:
|
|
466
|
+
"""
|
|
467
|
+
Update the MCP context with a new key-value pair.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
key: The context key
|
|
471
|
+
value: The context value
|
|
472
|
+
"""
|
|
473
|
+
if self._use_shared_context:
|
|
474
|
+
self._shared_context.set(key, value)
|
|
475
|
+
else:
|
|
476
|
+
self.context_store[key] = value
|
|
477
|
+
|
|
478
|
+
def get_context(self, key: str) -> Any:
|
|
479
|
+
"""
|
|
480
|
+
Get a value from the MCP context.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
key: The context key to retrieve
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
The context value or None if not found
|
|
487
|
+
"""
|
|
488
|
+
if self._use_shared_context:
|
|
489
|
+
return self._shared_context.get(key)
|
|
490
|
+
else:
|
|
491
|
+
return self.context_store.get(key)
|
|
492
|
+
|
|
493
|
+
def has_context(self, key: str) -> bool:
|
|
494
|
+
"""
|
|
495
|
+
Check if a key exists in the context.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
key: The key to check for
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
True if the key exists, False otherwise
|
|
502
|
+
"""
|
|
503
|
+
if self._use_shared_context:
|
|
504
|
+
return self._shared_context.has(key)
|
|
505
|
+
else:
|
|
506
|
+
return key in self.context_store
|
|
507
|
+
|
|
508
|
+
def list_available_tools(self) -> List[Dict]:
|
|
509
|
+
"""
|
|
510
|
+
Get a list of all available MCP tools.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
List of tool definitions
|
|
514
|
+
"""
|
|
515
|
+
return [
|
|
516
|
+
{
|
|
517
|
+
"name": name,
|
|
518
|
+
"description": tool["description"]
|
|
519
|
+
}
|
|
520
|
+
for name, tool in self.mcp_tools.items()
|
|
521
|
+
]
|
|
522
|
+
|
|
523
|
+
def add_tool(self, tool_func: Callable) -> None:
|
|
524
|
+
"""
|
|
525
|
+
Add a tool to this MCPNode.
|
|
526
|
+
|
|
527
|
+
This is a convenience method that calls register_mcp_tool
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
tool_func: The tool function to add
|
|
531
|
+
"""
|
|
532
|
+
self.register_mcp_tool(tool_func)
|
|
533
|
+
|
|
534
|
+
def execute_tool(self, tool_name: str, **kwargs) -> Any:
|
|
535
|
+
"""
|
|
536
|
+
Execute an MCP tool by name with the provided arguments.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
tool_name: The name of the tool to execute
|
|
540
|
+
**kwargs: Arguments to pass to the tool
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
The result of the tool execution
|
|
544
|
+
|
|
545
|
+
Raises:
|
|
546
|
+
ValueError: If the tool is not found
|
|
547
|
+
"""
|
|
548
|
+
if tool_name not in self.mcp_tools:
|
|
549
|
+
raise ValueError(f"Tool '{tool_name}' not found")
|
|
550
|
+
|
|
551
|
+
tool_func = self.mcp_tools[tool_name]["function"]
|
|
552
|
+
|
|
553
|
+
# Handle both old-style and new-style tool calling
|
|
554
|
+
try:
|
|
555
|
+
if hasattr(tool_func, "invoke"):
|
|
556
|
+
# New-style (LangChain 0.1.47+)
|
|
557
|
+
return tool_func.invoke(input=kwargs if kwargs else "")
|
|
558
|
+
else:
|
|
559
|
+
# Build a proper tool_input string for old-style tools
|
|
560
|
+
# For tools with no arguments, pass empty string
|
|
561
|
+
if not kwargs:
|
|
562
|
+
return tool_func("")
|
|
563
|
+
# For tools with arguments, format it properly
|
|
564
|
+
tool_input = ""
|
|
565
|
+
for k, v in kwargs.items():
|
|
566
|
+
tool_input += f"{k}: {v}, "
|
|
567
|
+
tool_input = tool_input.rstrip(", ")
|
|
568
|
+
return tool_func(tool_input)
|
|
569
|
+
except Exception as e:
|
|
570
|
+
# Fallback method - direct function call
|
|
571
|
+
# This works for simple Python functions that don't use the tool interface
|
|
572
|
+
if callable(tool_func) and not isinstance(tool_func, type):
|
|
573
|
+
return tool_func(**kwargs)
|
|
574
|
+
raise e
|
|
575
|
+
|
|
576
|
+
def get_system_message(self) -> str:
|
|
577
|
+
"""
|
|
578
|
+
Get the system message for this node, including context summary.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
The full system message with context
|
|
582
|
+
"""
|
|
583
|
+
base_message = self.get_context("system_message") or "You are an AI assistant."
|
|
584
|
+
context_summary = self._generate_context_summary()
|
|
585
|
+
|
|
586
|
+
if context_summary:
|
|
587
|
+
return f"{base_message}\n\nAvailable context:\n{context_summary}"
|
|
588
|
+
else:
|
|
589
|
+
return base_message
|
|
590
|
+
|
|
591
|
+
def _generate_context_summary(self) -> str:
|
|
592
|
+
"""
|
|
593
|
+
Generate a summary of available context for inclusion in the system message.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
String summary of available context
|
|
597
|
+
"""
|
|
598
|
+
# Get the context to summarize - either shared or local
|
|
599
|
+
if self._use_shared_context:
|
|
600
|
+
context_keys = self._shared_context.list_keys()
|
|
601
|
+
# Skip non-essential keys to prevent overwhelming the context
|
|
602
|
+
context_keys = [k for k in context_keys if not k.startswith("node_")]
|
|
603
|
+
|
|
604
|
+
if not context_keys:
|
|
605
|
+
return ""
|
|
606
|
+
|
|
607
|
+
summary_parts = []
|
|
608
|
+
for key in context_keys:
|
|
609
|
+
# Skip the system message in the summary
|
|
610
|
+
if key == "system_message":
|
|
611
|
+
continue
|
|
612
|
+
|
|
613
|
+
value = self._shared_context.get(key)
|
|
614
|
+
|
|
615
|
+
# For complex objects, just indicate their type
|
|
616
|
+
if isinstance(value, dict):
|
|
617
|
+
summary_parts.append(f"- {key}: Dictionary with {len(value)} items")
|
|
618
|
+
elif isinstance(value, list):
|
|
619
|
+
summary_parts.append(f"- {key}: List with {len(value)} items")
|
|
620
|
+
elif isinstance(value, str) and len(value) > 100:
|
|
621
|
+
summary_parts.append(f"- {key}: Text ({len(value)} chars)")
|
|
622
|
+
else:
|
|
623
|
+
summary_parts.append(f"- {key}: {value}")
|
|
624
|
+
else:
|
|
625
|
+
# Local context
|
|
626
|
+
if not self.context_store:
|
|
627
|
+
return ""
|
|
628
|
+
|
|
629
|
+
summary_parts = []
|
|
630
|
+
for key, value in self.context_store.items():
|
|
631
|
+
# Skip the system message in the summary
|
|
632
|
+
if key == "system_message":
|
|
633
|
+
continue
|
|
634
|
+
|
|
635
|
+
# For complex objects, just indicate their type
|
|
636
|
+
if isinstance(value, dict):
|
|
637
|
+
summary_parts.append(f"- {key}: Dictionary with {len(value)} items")
|
|
638
|
+
elif isinstance(value, list):
|
|
639
|
+
summary_parts.append(f"- {key}: List with {len(value)} items")
|
|
640
|
+
elif isinstance(value, str) and len(value) > 100:
|
|
641
|
+
summary_parts.append(f"- {key}: Text ({len(value)} chars)")
|
|
642
|
+
else:
|
|
643
|
+
summary_parts.append(f"- {key}: {value}")
|
|
644
|
+
|
|
645
|
+
return "\n".join(summary_parts)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class MCPReactAgent(MCPNode):
|
|
649
|
+
"""An implementation of MCP for LangGraph's ReAct agent pattern.
|
|
650
|
+
|
|
651
|
+
This class extends MCPNode to work specifically with ReAct agents,
|
|
652
|
+
providing a seamless integration of the Model Context Protocol with
|
|
653
|
+
LangGraph's agent architecture. It handles:
|
|
654
|
+
|
|
655
|
+
- Agent Creation: Creates ReAct agents with MCP context integration
|
|
656
|
+
- Tool Management: Combines MCP tools with custom agent tools
|
|
657
|
+
- Context Integration: Injects MCP context into agent's system messages
|
|
658
|
+
- LLM Compatibility: Handles different LLM implementations and versions
|
|
659
|
+
|
|
660
|
+
Example:
|
|
661
|
+
>>> agent = MCPReactAgent(name="my_agent")
|
|
662
|
+
>>> react_agent = agent.create_agent(llm, tools=[my_tool])
|
|
663
|
+
"""
|
|
664
|
+
|
|
665
|
+
def create_mcp_langgraph(
|
|
666
|
+
llm,
|
|
667
|
+
name: str = "MCPGraph",
|
|
668
|
+
system_message: Optional[str] = None,
|
|
669
|
+
tools: Optional[List] = None,
|
|
670
|
+
additional_nodes: Optional[Dict] = None,
|
|
671
|
+
**kwargs
|
|
672
|
+
) -> StateGraph:
|
|
673
|
+
"""Create a LangGraph with MCP capabilities.
|
|
674
|
+
|
|
675
|
+
This function creates a LangGraph that integrates the Model Context Protocol,
|
|
676
|
+
enabling context sharing and standardized tool usage across the graph. It:
|
|
677
|
+
|
|
678
|
+
- Creates an MCP-enabled ReAct agent as the primary node
|
|
679
|
+
- Configures the graph with proper routing and tool nodes
|
|
680
|
+
- Supports additional custom nodes and tools
|
|
681
|
+
- Handles LLM integration and system messages
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
llm: The language model to use
|
|
685
|
+
name: Name of the graph
|
|
686
|
+
system_message: System message for the agent
|
|
687
|
+
tools: Additional tools to provide to the agent
|
|
688
|
+
additional_nodes: Optional additional nodes to add to the graph
|
|
689
|
+
**kwargs: Additional keyword arguments
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
A configured StateGraph with MCP capabilities
|
|
693
|
+
"""
|
|
694
|
+
# Create MCP node
|
|
695
|
+
mcp_agent = MCPReactAgent(name=name, system_message=system_message)
|
|
696
|
+
|
|
697
|
+
# Create agent node
|
|
698
|
+
agent = mcp_agent.create_agent(llm, tools)
|
|
699
|
+
|
|
700
|
+
# Initialize the state graph
|
|
701
|
+
builder = StateGraph(cast(Type, Dict))
|
|
702
|
+
|
|
703
|
+
# Add the agent node
|
|
704
|
+
builder.add_node("agent", agent)
|
|
705
|
+
|
|
706
|
+
# Add any additional nodes
|
|
707
|
+
if additional_nodes:
|
|
708
|
+
for node_name, node in additional_nodes.items():
|
|
709
|
+
builder.add_node(node_name, node)
|
|
710
|
+
|
|
711
|
+
# Set the entry point
|
|
712
|
+
builder.set_entry_point("agent")
|
|
713
|
+
|
|
714
|
+
# Add conditional edges
|
|
715
|
+
# This simpler routing approach works better with the latest LangGraph
|
|
716
|
+
builder.add_edge("agent", END)
|
|
717
|
+
|
|
718
|
+
# Add any needed tools as nodes
|
|
719
|
+
tool_nodes = {}
|
|
720
|
+
|
|
721
|
+
# Skip adding tool nodes for now as they're causing compatibility issues
|
|
722
|
+
# LangGraph will handle tools internally within the agent
|
|
723
|
+
|
|
724
|
+
# Skip adding additional tool nodes for now
|
|
725
|
+
# The tools are already passed to the agent when it's created
|
|
726
|
+
|
|
727
|
+
# Compile the graph
|
|
728
|
+
graph = builder.compile()
|
|
729
|
+
|
|
730
|
+
# Store the MCP agent for later access
|
|
731
|
+
graph.mcp_agent = mcp_agent
|
|
732
|
+
|
|
733
|
+
return graph
|