kailash 0.1.5__py3-none-any.whl → 0.2.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.
- kailash/__init__.py +1 -1
- kailash/access_control.py +740 -0
- kailash/api/__main__.py +6 -0
- kailash/api/auth.py +668 -0
- kailash/api/custom_nodes.py +285 -0
- kailash/api/custom_nodes_secure.py +377 -0
- kailash/api/database.py +620 -0
- kailash/api/studio.py +915 -0
- kailash/api/studio_secure.py +893 -0
- kailash/mcp/__init__.py +53 -0
- kailash/mcp/__main__.py +13 -0
- kailash/mcp/ai_registry_server.py +712 -0
- kailash/mcp/client.py +447 -0
- kailash/mcp/client_new.py +334 -0
- kailash/mcp/server.py +293 -0
- kailash/mcp/server_new.py +336 -0
- kailash/mcp/servers/__init__.py +12 -0
- kailash/mcp/servers/ai_registry.py +289 -0
- kailash/nodes/__init__.py +4 -2
- kailash/nodes/ai/__init__.py +2 -0
- kailash/nodes/ai/a2a.py +714 -67
- kailash/nodes/ai/intelligent_agent_orchestrator.py +31 -37
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +5 -6
- kailash/nodes/base.py +15 -2
- kailash/nodes/base_async.py +45 -0
- kailash/nodes/base_cycle_aware.py +374 -0
- kailash/nodes/base_with_acl.py +338 -0
- kailash/nodes/code/python.py +135 -27
- kailash/nodes/data/readers.py +16 -6
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +187 -27
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- kailash/runtime/__init__.py +2 -1
- kailash/runtime/access_controlled.py +458 -0
- kailash/runtime/local.py +106 -33
- kailash/runtime/parallel_cyclic.py +529 -0
- kailash/sdk_exceptions.py +90 -5
- kailash/security.py +845 -0
- kailash/tracking/manager.py +38 -15
- kailash/tracking/models.py +1 -1
- kailash/tracking/storage/filesystem.py +30 -2
- kailash/utils/__init__.py +8 -0
- kailash/workflow/__init__.py +18 -0
- kailash/workflow/convergence.py +270 -0
- kailash/workflow/cycle_analyzer.py +768 -0
- kailash/workflow/cycle_builder.py +573 -0
- kailash/workflow/cycle_config.py +709 -0
- kailash/workflow/cycle_debugger.py +760 -0
- kailash/workflow/cycle_exceptions.py +601 -0
- kailash/workflow/cycle_profiler.py +671 -0
- kailash/workflow/cycle_state.py +338 -0
- kailash/workflow/cyclic_runner.py +985 -0
- kailash/workflow/graph.py +500 -39
- kailash/workflow/migration.py +768 -0
- kailash/workflow/safety.py +365 -0
- kailash/workflow/templates.py +744 -0
- kailash/workflow/validation.py +693 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/METADATA +256 -12
- kailash-0.2.0.dist-info/RECORD +125 -0
- kailash/nodes/mcp/__init__.py +0 -11
- kailash/nodes/mcp/client.py +0 -554
- kailash/nodes/mcp/resource.py +0 -682
- kailash/nodes/mcp/server.py +0 -577
- kailash-0.1.5.dist-info/RECORD +0 -88
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,334 @@
|
|
1
|
+
"""
|
2
|
+
MCP Client - Clean implementation using official Anthropic MCP SDK.
|
3
|
+
|
4
|
+
This is NOT a node - it's a utility class used by LLM agents to interact with MCP servers.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import os
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
10
|
+
|
11
|
+
# Will use official MCP SDK when available
|
12
|
+
try:
|
13
|
+
from mcp import ClientSession, StdioServerParameters
|
14
|
+
from mcp.client.stdio import stdio_client
|
15
|
+
|
16
|
+
MCP_AVAILABLE = True
|
17
|
+
except ImportError:
|
18
|
+
MCP_AVAILABLE = False
|
19
|
+
|
20
|
+
|
21
|
+
class MCPClient:
|
22
|
+
"""
|
23
|
+
Clean MCP client for connecting to Model Context Protocol servers.
|
24
|
+
|
25
|
+
This client is used internally by LLMAgentNode and other components
|
26
|
+
that need MCP capabilities. It provides a simple interface over the
|
27
|
+
official MCP SDK.
|
28
|
+
|
29
|
+
Examples:
|
30
|
+
>>> client = MCPClient()
|
31
|
+
>>>
|
32
|
+
>>> # Connect to a server
|
33
|
+
>>> async with client.connect_stdio("python", ["-m", "my_mcp_server"]) as session:
|
34
|
+
... # Discover available tools
|
35
|
+
... tools = await client.discover_tools(session)
|
36
|
+
...
|
37
|
+
... # Call a tool
|
38
|
+
... result = await client.call_tool(
|
39
|
+
... session,
|
40
|
+
... "search",
|
41
|
+
... {"query": "AI in healthcare"}
|
42
|
+
... )
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(self):
|
46
|
+
"""Initialize MCP client."""
|
47
|
+
self.logger = logging.getLogger(__name__)
|
48
|
+
|
49
|
+
async def connect_stdio(
|
50
|
+
self, command: str, args: List[str], env: Optional[Dict[str, str]] = None
|
51
|
+
):
|
52
|
+
"""
|
53
|
+
Connect to an MCP server via stdio transport.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
command: Command to run the server
|
57
|
+
args: Arguments for the command
|
58
|
+
env: Environment variables
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Context manager yielding ClientSession
|
62
|
+
|
63
|
+
Raises:
|
64
|
+
ImportError: If MCP SDK is not available
|
65
|
+
RuntimeError: If connection fails
|
66
|
+
"""
|
67
|
+
if not MCP_AVAILABLE:
|
68
|
+
raise ImportError("MCP SDK not available. Install with: pip install mcp")
|
69
|
+
|
70
|
+
# Merge environment
|
71
|
+
server_env = os.environ.copy()
|
72
|
+
if env:
|
73
|
+
server_env.update(env)
|
74
|
+
|
75
|
+
# Create server parameters
|
76
|
+
server_params = StdioServerParameters(
|
77
|
+
command=command, args=args, env=server_env
|
78
|
+
)
|
79
|
+
|
80
|
+
try:
|
81
|
+
# Connect to server
|
82
|
+
async with stdio_client(server_params) as (read_stream, write_stream):
|
83
|
+
async with ClientSession(read_stream, write_stream) as session:
|
84
|
+
# Initialize session
|
85
|
+
await session.initialize()
|
86
|
+
yield session
|
87
|
+
except Exception as e:
|
88
|
+
self.logger.error(f"Failed to connect to MCP server: {e}")
|
89
|
+
raise RuntimeError(f"MCP connection failed: {e}")
|
90
|
+
|
91
|
+
async def discover_tools(self, session: "ClientSession") -> List[Dict[str, Any]]:
|
92
|
+
"""
|
93
|
+
Discover available tools from an MCP server.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
session: Active MCP client session
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
List of tool definitions with name, description, and schema
|
100
|
+
"""
|
101
|
+
try:
|
102
|
+
result = await session.list_tools()
|
103
|
+
tools = []
|
104
|
+
|
105
|
+
for tool in result.tools:
|
106
|
+
tools.append(
|
107
|
+
{
|
108
|
+
"name": tool.name,
|
109
|
+
"description": tool.description,
|
110
|
+
"inputSchema": tool.inputSchema,
|
111
|
+
}
|
112
|
+
)
|
113
|
+
|
114
|
+
return tools
|
115
|
+
|
116
|
+
except Exception as e:
|
117
|
+
self.logger.error(f"Failed to discover tools: {e}")
|
118
|
+
return []
|
119
|
+
|
120
|
+
async def call_tool(
|
121
|
+
self, session: "ClientSession", name: str, arguments: Dict[str, Any]
|
122
|
+
) -> Any:
|
123
|
+
"""
|
124
|
+
Call a tool on the MCP server.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
session: Active MCP client session
|
128
|
+
name: Tool name
|
129
|
+
arguments: Tool arguments
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
Tool execution result
|
133
|
+
"""
|
134
|
+
try:
|
135
|
+
result = await session.call_tool(name=name, arguments=arguments)
|
136
|
+
|
137
|
+
# Extract content from result
|
138
|
+
if hasattr(result, "content"):
|
139
|
+
content = []
|
140
|
+
for item in result.content:
|
141
|
+
if hasattr(item, "text"):
|
142
|
+
content.append({"type": "text", "text": item.text})
|
143
|
+
else:
|
144
|
+
content.append(str(item))
|
145
|
+
return content
|
146
|
+
else:
|
147
|
+
return str(result)
|
148
|
+
|
149
|
+
except Exception as e:
|
150
|
+
self.logger.error(f"Failed to call tool '{name}': {e}")
|
151
|
+
raise
|
152
|
+
|
153
|
+
async def list_resources(self, session: "ClientSession") -> List[Dict[str, Any]]:
|
154
|
+
"""
|
155
|
+
List available resources from an MCP server.
|
156
|
+
|
157
|
+
Args:
|
158
|
+
session: Active MCP client session
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
List of resource definitions
|
162
|
+
"""
|
163
|
+
try:
|
164
|
+
result = await session.list_resources()
|
165
|
+
resources = []
|
166
|
+
|
167
|
+
for resource in result.resources:
|
168
|
+
resources.append(
|
169
|
+
{
|
170
|
+
"uri": resource.uri,
|
171
|
+
"name": resource.name,
|
172
|
+
"description": resource.description,
|
173
|
+
"mimeType": resource.mimeType,
|
174
|
+
}
|
175
|
+
)
|
176
|
+
|
177
|
+
return resources
|
178
|
+
|
179
|
+
except Exception as e:
|
180
|
+
self.logger.error(f"Failed to list resources: {e}")
|
181
|
+
return []
|
182
|
+
|
183
|
+
async def read_resource(self, session: "ClientSession", uri: str) -> Any:
|
184
|
+
"""
|
185
|
+
Read a specific resource from an MCP server.
|
186
|
+
|
187
|
+
Args:
|
188
|
+
session: Active MCP client session
|
189
|
+
uri: Resource URI
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
Resource content
|
193
|
+
"""
|
194
|
+
try:
|
195
|
+
result = await session.read_resource(uri=uri)
|
196
|
+
|
197
|
+
# Extract content
|
198
|
+
if hasattr(result, "contents"):
|
199
|
+
content = []
|
200
|
+
for item in result.contents:
|
201
|
+
if hasattr(item, "text"):
|
202
|
+
content.append({"type": "text", "text": item.text})
|
203
|
+
elif hasattr(item, "blob"):
|
204
|
+
content.append({"type": "blob", "data": item.blob})
|
205
|
+
else:
|
206
|
+
content.append(str(item))
|
207
|
+
return content
|
208
|
+
else:
|
209
|
+
return str(result)
|
210
|
+
|
211
|
+
except Exception as e:
|
212
|
+
self.logger.error(f"Failed to read resource '{uri}': {e}")
|
213
|
+
raise
|
214
|
+
|
215
|
+
async def list_prompts(self, session: "ClientSession") -> List[Dict[str, Any]]:
|
216
|
+
"""
|
217
|
+
List available prompts from an MCP server.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
session: Active MCP client session
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
List of prompt definitions
|
224
|
+
"""
|
225
|
+
try:
|
226
|
+
result = await session.list_prompts()
|
227
|
+
prompts = []
|
228
|
+
|
229
|
+
for prompt in result.prompts:
|
230
|
+
prompt_dict = {
|
231
|
+
"name": prompt.name,
|
232
|
+
"description": prompt.description,
|
233
|
+
"arguments": [],
|
234
|
+
}
|
235
|
+
|
236
|
+
if hasattr(prompt, "arguments"):
|
237
|
+
for arg in prompt.arguments:
|
238
|
+
prompt_dict["arguments"].append(
|
239
|
+
{
|
240
|
+
"name": arg.name,
|
241
|
+
"description": arg.description,
|
242
|
+
"required": arg.required,
|
243
|
+
}
|
244
|
+
)
|
245
|
+
|
246
|
+
prompts.append(prompt_dict)
|
247
|
+
|
248
|
+
return prompts
|
249
|
+
|
250
|
+
except Exception as e:
|
251
|
+
self.logger.error(f"Failed to list prompts: {e}")
|
252
|
+
return []
|
253
|
+
|
254
|
+
async def get_prompt(
|
255
|
+
self, session: "ClientSession", name: str, arguments: Dict[str, Any]
|
256
|
+
) -> Dict[str, Any]:
|
257
|
+
"""
|
258
|
+
Get a prompt from an MCP server.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
session: Active MCP client session
|
262
|
+
name: Prompt name
|
263
|
+
arguments: Prompt arguments
|
264
|
+
|
265
|
+
Returns:
|
266
|
+
Prompt with messages
|
267
|
+
"""
|
268
|
+
try:
|
269
|
+
result = await session.get_prompt(name=name, arguments=arguments)
|
270
|
+
|
271
|
+
# Extract messages
|
272
|
+
messages = []
|
273
|
+
if hasattr(result, "messages"):
|
274
|
+
for msg in result.messages:
|
275
|
+
messages.append(
|
276
|
+
{
|
277
|
+
"role": msg.role,
|
278
|
+
"content": (
|
279
|
+
msg.content.text
|
280
|
+
if hasattr(msg.content, "text")
|
281
|
+
else str(msg.content)
|
282
|
+
),
|
283
|
+
}
|
284
|
+
)
|
285
|
+
|
286
|
+
return {"name": name, "messages": messages, "arguments": arguments}
|
287
|
+
|
288
|
+
except Exception as e:
|
289
|
+
self.logger.error(f"Failed to get prompt '{name}': {e}")
|
290
|
+
raise
|
291
|
+
|
292
|
+
|
293
|
+
# Convenience functions for LLM agents
|
294
|
+
async def discover_and_prepare_tools(
|
295
|
+
mcp_servers: List[Union[str, Dict[str, Any]]]
|
296
|
+
) -> List[Dict[str, Any]]:
|
297
|
+
"""
|
298
|
+
Discover tools from multiple MCP servers and prepare them for LLM use.
|
299
|
+
|
300
|
+
Args:
|
301
|
+
mcp_servers: List of server URLs or configurations
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
List of tool definitions ready for LLM function calling
|
305
|
+
"""
|
306
|
+
client = MCPClient()
|
307
|
+
all_tools = []
|
308
|
+
|
309
|
+
for server in mcp_servers:
|
310
|
+
try:
|
311
|
+
# Parse server configuration
|
312
|
+
if isinstance(server, str):
|
313
|
+
# Simple URL format - not supported in stdio
|
314
|
+
continue
|
315
|
+
|
316
|
+
if server.get("transport") == "stdio":
|
317
|
+
command = server.get("command", "python")
|
318
|
+
args = server.get("args", [])
|
319
|
+
env = server.get("env", {})
|
320
|
+
|
321
|
+
async with client.connect_stdio(command, args, env) as session:
|
322
|
+
tools = await client.discover_tools(session)
|
323
|
+
|
324
|
+
# Tag tools with server info
|
325
|
+
for tool in tools:
|
326
|
+
tool["server"] = server.get("name", "mcp_server")
|
327
|
+
tool["server_config"] = server
|
328
|
+
|
329
|
+
all_tools.extend(tools)
|
330
|
+
|
331
|
+
except Exception as e:
|
332
|
+
logging.warning(f"Failed to discover tools from {server}: {e}")
|
333
|
+
|
334
|
+
return all_tools
|
kailash/mcp/server.py
ADDED
@@ -0,0 +1,293 @@
|
|
1
|
+
"""MCP Server Framework using official Anthropic SDK.
|
2
|
+
|
3
|
+
This module provides a comprehensive framework for creating MCP servers using
|
4
|
+
the official FastMCP framework from Anthropic. Servers run as long-lived
|
5
|
+
services that expose tools, resources, and prompts to MCP clients, enabling
|
6
|
+
dynamic capability extension for AI workflows.
|
7
|
+
|
8
|
+
Note:
|
9
|
+
This module requires the FastMCP framework to be installed.
|
10
|
+
Install with: pip install 'mcp[server]'
|
11
|
+
|
12
|
+
Examples:
|
13
|
+
Basic server with tools:
|
14
|
+
|
15
|
+
>>> from kailash.mcp.server import MCPServer
|
16
|
+
>>> class MyServer(MCPServer):
|
17
|
+
... def setup(self):
|
18
|
+
... @self.add_tool()
|
19
|
+
... def calculate(a: int, b: int) -> int:
|
20
|
+
... return a + b
|
21
|
+
>>> server = MyServer("calculator", port=8080)
|
22
|
+
>>> server.start()
|
23
|
+
|
24
|
+
Quick server creation:
|
25
|
+
|
26
|
+
>>> from kailash.mcp.server import SimpleMCPServer
|
27
|
+
>>> server = SimpleMCPServer("my-tools")
|
28
|
+
>>> @server.tool()
|
29
|
+
... def search(query: str) -> list:
|
30
|
+
... return [f"Result for {query}"]
|
31
|
+
>>> server.start()
|
32
|
+
"""
|
33
|
+
|
34
|
+
import logging
|
35
|
+
from abc import ABC, abstractmethod
|
36
|
+
from typing import Callable
|
37
|
+
|
38
|
+
logger = logging.getLogger(__name__)
|
39
|
+
|
40
|
+
|
41
|
+
class MCPServer(ABC):
|
42
|
+
"""Base class for MCP servers using FastMCP.
|
43
|
+
|
44
|
+
This provides a framework for creating MCP servers that expose
|
45
|
+
tools, resources, and prompts via the Model Context Protocol.
|
46
|
+
|
47
|
+
Examples:
|
48
|
+
Creating a custom server:
|
49
|
+
|
50
|
+
>>> class MyServer(MCPServer):
|
51
|
+
... def setup(self):
|
52
|
+
... @self.add_tool()
|
53
|
+
... def search(query: str) -> str:
|
54
|
+
... return f"Results for: {query}"
|
55
|
+
... @self.add_resource("data://example")
|
56
|
+
... def get_example():
|
57
|
+
... return "Example data"
|
58
|
+
>>> server = MyServer("my-server", port=8080)
|
59
|
+
>>> server.start() # Runs until stopped
|
60
|
+
"""
|
61
|
+
|
62
|
+
def __init__(self, name: str, port: int = 8080, host: str = "localhost"):
|
63
|
+
"""Initialize the MCP server.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
name: Name of the server.
|
67
|
+
port: Port to listen on (default: 8080).
|
68
|
+
host: Host to bind to (default: "localhost").
|
69
|
+
"""
|
70
|
+
self.name = name
|
71
|
+
self.port = port
|
72
|
+
self.host = host
|
73
|
+
self._mcp = None
|
74
|
+
self._running = False
|
75
|
+
|
76
|
+
@abstractmethod
|
77
|
+
def setup(self):
|
78
|
+
"""Setup server tools, resources, and prompts.
|
79
|
+
|
80
|
+
This method should be implemented by subclasses to define
|
81
|
+
the server's capabilities using decorators.
|
82
|
+
|
83
|
+
Note:
|
84
|
+
Use @self.add_tool(), @self.add_resource(uri), and
|
85
|
+
@self.add_prompt(name) decorators to register capabilities.
|
86
|
+
"""
|
87
|
+
pass
|
88
|
+
|
89
|
+
def add_tool(self):
|
90
|
+
"""Decorator to add a tool to the server.
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
Function decorator for registering tools.
|
94
|
+
|
95
|
+
Examples:
|
96
|
+
>>> @server.add_tool()
|
97
|
+
... def calculate(a: int, b: int) -> int:
|
98
|
+
... '''Add two numbers'''
|
99
|
+
... return a + b
|
100
|
+
"""
|
101
|
+
|
102
|
+
def decorator(func: Callable):
|
103
|
+
if self._mcp is None:
|
104
|
+
self._init_mcp()
|
105
|
+
|
106
|
+
# Use FastMCP's tool decorator
|
107
|
+
return self._mcp.tool()(func)
|
108
|
+
|
109
|
+
return decorator
|
110
|
+
|
111
|
+
def add_resource(self, uri: str):
|
112
|
+
"""Decorator to add a resource to the server.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
uri: URI pattern for the resource (supports wildcards).
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
Function decorator for registering resources.
|
119
|
+
|
120
|
+
Examples:
|
121
|
+
>>> @server.add_resource("file:///data/*")
|
122
|
+
... def get_file(path: str) -> str:
|
123
|
+
... return f"Content of {path}"
|
124
|
+
"""
|
125
|
+
|
126
|
+
def decorator(func: Callable):
|
127
|
+
if self._mcp is None:
|
128
|
+
self._init_mcp()
|
129
|
+
|
130
|
+
# Use FastMCP's resource decorator
|
131
|
+
return self._mcp.resource(uri)(func)
|
132
|
+
|
133
|
+
return decorator
|
134
|
+
|
135
|
+
def add_prompt(self, name: str):
|
136
|
+
"""Decorator to add a prompt template to the server.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
name: Name of the prompt.
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
Function decorator for registering prompts.
|
143
|
+
|
144
|
+
Examples:
|
145
|
+
>>> @server.add_prompt("analyze")
|
146
|
+
... def analyze_prompt(data: str) -> str:
|
147
|
+
... return f"Please analyze the following data: {data}"
|
148
|
+
"""
|
149
|
+
|
150
|
+
def decorator(func: Callable):
|
151
|
+
if self._mcp is None:
|
152
|
+
self._init_mcp()
|
153
|
+
|
154
|
+
# Use FastMCP's prompt decorator
|
155
|
+
return self._mcp.prompt(name)(func)
|
156
|
+
|
157
|
+
return decorator
|
158
|
+
|
159
|
+
def _init_mcp(self):
|
160
|
+
"""Initialize the FastMCP instance."""
|
161
|
+
try:
|
162
|
+
from mcp.server.fastmcp import FastMCP
|
163
|
+
|
164
|
+
self._mcp = FastMCP(self.name)
|
165
|
+
except ImportError:
|
166
|
+
logger.error(
|
167
|
+
"FastMCP not available. Install with: pip install 'mcp[server]'"
|
168
|
+
)
|
169
|
+
raise
|
170
|
+
|
171
|
+
def start(self):
|
172
|
+
"""Start the MCP server.
|
173
|
+
|
174
|
+
This runs the server as a long-lived process until stopped.
|
175
|
+
|
176
|
+
Raises:
|
177
|
+
ImportError: If FastMCP is not available.
|
178
|
+
Exception: If server fails to start.
|
179
|
+
"""
|
180
|
+
if self._mcp is None:
|
181
|
+
self._init_mcp()
|
182
|
+
|
183
|
+
# Run setup to register tools/resources
|
184
|
+
self.setup()
|
185
|
+
|
186
|
+
logger.info(f"Starting MCP server '{self.name}' on {self.host}:{self.port}")
|
187
|
+
self._running = True
|
188
|
+
|
189
|
+
try:
|
190
|
+
# Run the FastMCP server
|
191
|
+
logger.info("Running FastMCP server in stdio mode")
|
192
|
+
self._mcp.run()
|
193
|
+
except Exception as e:
|
194
|
+
logger.error(f"Failed to start server: {e}")
|
195
|
+
raise
|
196
|
+
finally:
|
197
|
+
self._running = False
|
198
|
+
|
199
|
+
def stop(self):
|
200
|
+
"""Stop the MCP server."""
|
201
|
+
logger.info(f"Stopping MCP server '{self.name}'")
|
202
|
+
self._running = False
|
203
|
+
# In a real implementation, we'd need to handle graceful shutdown
|
204
|
+
|
205
|
+
|
206
|
+
class SimpleMCPServer(MCPServer):
|
207
|
+
"""Simple MCP server for basic use cases.
|
208
|
+
|
209
|
+
This provides an easy way to create MCP servers without subclassing.
|
210
|
+
|
211
|
+
Examples:
|
212
|
+
>>> server = SimpleMCPServer("my-server")
|
213
|
+
>>> @server.tool()
|
214
|
+
... def add(a: int, b: int) -> int:
|
215
|
+
... return a + b
|
216
|
+
>>> server.start()
|
217
|
+
"""
|
218
|
+
|
219
|
+
def __init__(self, name: str, port: int = 8080, host: str = "localhost"):
|
220
|
+
"""Initialize the simple MCP server.
|
221
|
+
|
222
|
+
Args:
|
223
|
+
name: Name of the server.
|
224
|
+
port: Port to listen on (default: 8080).
|
225
|
+
host: Host to bind to (default: "localhost").
|
226
|
+
"""
|
227
|
+
super().__init__(name, port, host)
|
228
|
+
self._tools = []
|
229
|
+
self._resources = []
|
230
|
+
self._prompts = []
|
231
|
+
|
232
|
+
def tool(self):
|
233
|
+
"""Decorator to add a tool.
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
Function decorator for registering tools.
|
237
|
+
"""
|
238
|
+
|
239
|
+
def decorator(func):
|
240
|
+
self._tools.append(func)
|
241
|
+
return func
|
242
|
+
|
243
|
+
return decorator
|
244
|
+
|
245
|
+
def resource(self, uri: str):
|
246
|
+
"""Decorator to add a resource.
|
247
|
+
|
248
|
+
Args:
|
249
|
+
uri: URI pattern for the resource.
|
250
|
+
|
251
|
+
Returns:
|
252
|
+
Function decorator for registering resources.
|
253
|
+
"""
|
254
|
+
|
255
|
+
def decorator(func):
|
256
|
+
self._resources.append((uri, func))
|
257
|
+
return func
|
258
|
+
|
259
|
+
return decorator
|
260
|
+
|
261
|
+
def prompt(self, name: str):
|
262
|
+
"""Decorator to add a prompt.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
name: Name of the prompt.
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
Function decorator for registering prompts.
|
269
|
+
"""
|
270
|
+
|
271
|
+
def decorator(func):
|
272
|
+
self._prompts.append((name, func))
|
273
|
+
return func
|
274
|
+
|
275
|
+
return decorator
|
276
|
+
|
277
|
+
def setup(self):
|
278
|
+
"""Setup the server with registered components.
|
279
|
+
|
280
|
+
Registers all tools, resources, and prompts that were decorated
|
281
|
+
before calling start().
|
282
|
+
"""
|
283
|
+
# Register all tools
|
284
|
+
for tool_func in self._tools:
|
285
|
+
self.add_tool()(tool_func)
|
286
|
+
|
287
|
+
# Register all resources
|
288
|
+
for uri, resource_func in self._resources:
|
289
|
+
self.add_resource(uri)(resource_func)
|
290
|
+
|
291
|
+
# Register all prompts
|
292
|
+
for name, prompt_func in self._prompts:
|
293
|
+
self.add_prompt(name)(prompt_func)
|