robyn 0.73.0__cp311-cp311-macosx_10_12_x86_64.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.

Potentially problematic release.


This version of robyn might be problematic. Click here for more details.

Files changed (57) hide show
  1. robyn/__init__.py +757 -0
  2. robyn/__main__.py +4 -0
  3. robyn/ai.py +308 -0
  4. robyn/argument_parser.py +129 -0
  5. robyn/authentication.py +96 -0
  6. robyn/cli.py +136 -0
  7. robyn/dependency_injection.py +71 -0
  8. robyn/env_populator.py +35 -0
  9. robyn/events.py +6 -0
  10. robyn/exceptions.py +32 -0
  11. robyn/jsonify.py +13 -0
  12. robyn/logger.py +80 -0
  13. robyn/mcp.py +461 -0
  14. robyn/openapi.py +448 -0
  15. robyn/processpool.py +226 -0
  16. robyn/py.typed +0 -0
  17. robyn/reloader.py +164 -0
  18. robyn/responses.py +208 -0
  19. robyn/robyn.cpython-311-darwin.so +0 -0
  20. robyn/robyn.pyi +421 -0
  21. robyn/router.py +410 -0
  22. robyn/scaffold/mongo/Dockerfile +12 -0
  23. robyn/scaffold/mongo/app.py +43 -0
  24. robyn/scaffold/mongo/requirements.txt +2 -0
  25. robyn/scaffold/no-db/Dockerfile +12 -0
  26. robyn/scaffold/no-db/app.py +12 -0
  27. robyn/scaffold/no-db/requirements.txt +1 -0
  28. robyn/scaffold/postgres/Dockerfile +32 -0
  29. robyn/scaffold/postgres/app.py +31 -0
  30. robyn/scaffold/postgres/requirements.txt +3 -0
  31. robyn/scaffold/postgres/supervisord.conf +14 -0
  32. robyn/scaffold/prisma/Dockerfile +15 -0
  33. robyn/scaffold/prisma/app.py +32 -0
  34. robyn/scaffold/prisma/requirements.txt +2 -0
  35. robyn/scaffold/prisma/schema.prisma +13 -0
  36. robyn/scaffold/sqlalchemy/Dockerfile +12 -0
  37. robyn/scaffold/sqlalchemy/__init__.py +0 -0
  38. robyn/scaffold/sqlalchemy/app.py +13 -0
  39. robyn/scaffold/sqlalchemy/models.py +21 -0
  40. robyn/scaffold/sqlalchemy/requirements.txt +2 -0
  41. robyn/scaffold/sqlite/Dockerfile +12 -0
  42. robyn/scaffold/sqlite/app.py +22 -0
  43. robyn/scaffold/sqlite/requirements.txt +1 -0
  44. robyn/scaffold/sqlmodel/Dockerfile +11 -0
  45. robyn/scaffold/sqlmodel/app.py +46 -0
  46. robyn/scaffold/sqlmodel/models.py +10 -0
  47. robyn/scaffold/sqlmodel/requirements.txt +2 -0
  48. robyn/status_codes.py +137 -0
  49. robyn/swagger.html +32 -0
  50. robyn/templating.py +30 -0
  51. robyn/types.py +44 -0
  52. robyn/ws.py +67 -0
  53. robyn-0.73.0.dist-info/METADATA +32 -0
  54. robyn-0.73.0.dist-info/RECORD +57 -0
  55. robyn-0.73.0.dist-info/WHEEL +4 -0
  56. robyn-0.73.0.dist-info/entry_points.txt +3 -0
  57. robyn-0.73.0.dist-info/licenses/LICENSE +25 -0
robyn/mcp.py ADDED
@@ -0,0 +1,461 @@
1
+ """
2
+ Model Context Protocol (MCP) implementation for Robyn.
3
+
4
+ This module provides MCP server functionality following the JSON-RPC 2.0 specification.
5
+ It allows Robyn applications to serve as MCP servers, exposing resources, tools, and prompts
6
+ to MCP clients like Claude Desktop or other AI applications.
7
+ """
8
+
9
+ import inspect
10
+ import json
11
+ import logging
12
+ import re
13
+ from dataclasses import asdict, dataclass
14
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _extract_uri_params(uri: str) -> List[str]:
20
+ """Extract parameter names from URI template like 'echo://{message}'"""
21
+ return re.findall(r"\{(\w+)\}", uri)
22
+
23
+
24
+ def _generate_schema_from_function(func: Callable) -> Dict[str, Any]:
25
+ """Generate JSON schema from function signature"""
26
+ sig = inspect.signature(func)
27
+ properties = {}
28
+ required = []
29
+
30
+ for param_name, param in sig.parameters.items():
31
+ # Skip 'self' parameter
32
+ if param_name == "self":
33
+ continue
34
+
35
+ param_schema = {"type": "string"} # Default to string
36
+
37
+ # Try to infer type from annotation
38
+ if param.annotation != inspect.Parameter.empty:
39
+ if param.annotation is str:
40
+ param_schema["type"] = "string"
41
+ elif param.annotation is int:
42
+ param_schema["type"] = "integer"
43
+ elif param.annotation is float:
44
+ param_schema["type"] = "number"
45
+ elif param.annotation is bool:
46
+ param_schema["type"] = "boolean"
47
+ elif hasattr(param.annotation, "__origin__"):
48
+ # Handle generic types like List, Dict, etc.
49
+ param_schema["type"] = "object"
50
+
51
+ properties[param_name] = param_schema
52
+
53
+ # Add to required if no default value
54
+ if param.default == inspect.Parameter.empty:
55
+ required.append(param_name)
56
+
57
+ return {"type": "object", "properties": properties, "required": required}
58
+
59
+
60
+ def _generate_prompt_args_from_function(func: Callable) -> List[Dict[str, Any]]:
61
+ """Generate prompt arguments from function signature"""
62
+ sig = inspect.signature(func)
63
+ arguments = []
64
+
65
+ for param_name, param in sig.parameters.items():
66
+ if param_name == "self":
67
+ continue
68
+
69
+ arg_def = {"name": param_name, "description": f"Parameter {param_name}", "required": param.default == inspect.Parameter.empty}
70
+ arguments.append(arg_def)
71
+
72
+ return arguments
73
+
74
+
75
+ @dataclass
76
+ class MCPResource:
77
+ """Represents an MCP resource"""
78
+
79
+ uri: str
80
+ name: str
81
+ description: Optional[str] = None
82
+ mimeType: Optional[str] = None
83
+
84
+
85
+ @dataclass
86
+ class MCPTool:
87
+ """Represents an MCP tool"""
88
+
89
+ name: str
90
+ description: str
91
+ inputSchema: Dict[str, Any]
92
+
93
+
94
+ @dataclass
95
+ class MCPPrompt:
96
+ """Represents an MCP prompt template"""
97
+
98
+ name: str
99
+ description: str
100
+ arguments: Optional[List[Dict[str, Any]]] = None
101
+
102
+
103
+ @dataclass
104
+ class MCPMessage:
105
+ """JSON-RPC 2.0 message structure"""
106
+
107
+ jsonrpc: str = "2.0"
108
+ id: Optional[Union[str, int]] = None
109
+ method: Optional[str] = None
110
+ params: Optional[Dict[str, Any]] = None
111
+ result: Optional[Any] = None
112
+ error: Optional[Dict[str, Any]] = None
113
+
114
+
115
+ class MCPError(Exception):
116
+ """MCP-specific error"""
117
+
118
+ def __init__(self, code: int, message: str, data: Optional[Any] = None):
119
+ self.code = code
120
+ self.message = message
121
+ self.data = data
122
+ super().__init__(message)
123
+
124
+
125
+ class MCPHandler:
126
+ """Handles MCP protocol requests and responses"""
127
+
128
+ def __init__(self):
129
+ self.resources: Dict[str, Callable] = {}
130
+ self.tools: Dict[str, Callable] = {}
131
+ self.prompts: Dict[str, Callable] = {}
132
+ self.resource_metadata: Dict[str, MCPResource] = {}
133
+ self.tool_metadata: Dict[str, MCPTool] = {}
134
+ self.prompt_metadata: Dict[str, MCPPrompt] = {}
135
+ self.server_info = {"name": "robyn-mcp-server", "version": "1.0.0"}
136
+
137
+ def register_resource(self, uri: str, name: str, handler: Callable, description: Optional[str] = None, mime_type: Optional[str] = None):
138
+ """Register a resource handler"""
139
+ self.resources[uri] = handler
140
+ self.resource_metadata[uri] = MCPResource(uri=uri, name=name, description=description, mimeType=mime_type)
141
+
142
+ def register_tool(self, name: str, handler: Callable, description: str, input_schema: Dict[str, Any]):
143
+ """Register a tool handler"""
144
+ self.tools[name] = handler
145
+ self.tool_metadata[name] = MCPTool(name=name, description=description, inputSchema=input_schema)
146
+
147
+ def register_prompt(self, name: str, handler: Callable, description: str, arguments: Optional[List[Dict[str, Any]]] = None):
148
+ """Register a prompt handler"""
149
+ self.prompts[name] = handler
150
+ self.prompt_metadata[name] = MCPPrompt(name=name, description=description, arguments=arguments)
151
+
152
+ async def handle_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
153
+ """Handle an MCP JSON-RPC request"""
154
+ try:
155
+ # Parse the request
156
+ method = request_data.get("method")
157
+ params = request_data.get("params", {})
158
+ request_id = request_data.get("id")
159
+
160
+ # Handle different MCP methods
161
+ if method == "initialize":
162
+ result = await self._handle_initialize(params)
163
+ elif method == "resources/list":
164
+ result = await self._handle_list_resources(params)
165
+ elif method == "resources/read":
166
+ result = await self._handle_read_resource(params)
167
+ elif method == "tools/list":
168
+ result = await self._handle_list_tools(params)
169
+ elif method == "tools/call":
170
+ result = await self._handle_call_tool(params)
171
+ elif method == "prompts/list":
172
+ result = await self._handle_list_prompts(params)
173
+ elif method == "prompts/get":
174
+ result = await self._handle_get_prompt(params)
175
+ else:
176
+ raise MCPError(-32601, f"Method not found: {method}")
177
+
178
+ # Return successful response
179
+ return {"jsonrpc": "2.0", "id": request_id, "result": result}
180
+
181
+ except MCPError as e:
182
+ return {"jsonrpc": "2.0", "id": request_data.get("id"), "error": {"code": e.code, "message": e.message, "data": e.data}}
183
+ except Exception as e:
184
+ logger.exception("Error handling MCP request")
185
+ return {"jsonrpc": "2.0", "id": request_data.get("id"), "error": {"code": -32603, "message": "Internal error", "data": str(e)}}
186
+
187
+ async def _handle_initialize(self, params: Dict[str, Any]) -> Dict[str, Any]:
188
+ """Handle MCP initialize request"""
189
+ return {
190
+ "protocolVersion": "2024-11-05",
191
+ "capabilities": {"resources": {"subscribe": False, "listChanged": False}, "tools": {}, "prompts": {}},
192
+ "serverInfo": self.server_info,
193
+ }
194
+
195
+ async def _handle_list_resources(self, params: Dict[str, Any]) -> Dict[str, Any]:
196
+ """Handle resources/list request"""
197
+ resources = []
198
+ for uri, metadata in self.resource_metadata.items():
199
+ resources.append(asdict(metadata))
200
+ return {"resources": resources}
201
+
202
+ def _match_uri_template(self, requested_uri: str) -> Optional[Tuple[str, Dict[str, str]]]:
203
+ """Match requested URI against registered URI templates"""
204
+ for template_uri in self.resources.keys():
205
+ # Extract parameter names from template
206
+ param_names = _extract_uri_params(template_uri)
207
+
208
+ if not param_names:
209
+ # Exact match for non-templated URIs
210
+ if requested_uri == template_uri:
211
+ return template_uri, {}
212
+ continue
213
+
214
+ # Create regex pattern from template
215
+ pattern = template_uri
216
+ for param_name in param_names:
217
+ # Use (.+) to match any characters including slashes for paths
218
+ # Use ([^/]+) for single segments
219
+ if param_name in ["path", "file_path", "directory"]:
220
+ pattern = pattern.replace(f"{{{param_name}}}", r"(.+)")
221
+ else:
222
+ pattern = pattern.replace(f"{{{param_name}}}", r"([^/]+)")
223
+
224
+ match = re.match(f"^{pattern}$", requested_uri)
225
+ if match:
226
+ # Extract parameter values
227
+ param_values = {}
228
+ for i, param_name in enumerate(param_names):
229
+ param_values[param_name] = match.group(i + 1)
230
+ return template_uri, param_values
231
+
232
+ return None
233
+
234
+ async def _handle_read_resource(self, params: Dict[str, Any]) -> Dict[str, Any]:
235
+ """Handle resources/read request"""
236
+ uri = params.get("uri")
237
+ if not uri:
238
+ raise MCPError(-32602, "URI is required")
239
+
240
+ # Try to match URI template
241
+ match_result = self._match_uri_template(uri)
242
+ if not match_result:
243
+ raise MCPError(-32602, f"Resource not found: {uri}")
244
+
245
+ template_uri, uri_params = match_result
246
+ handler = self.resources[template_uri]
247
+
248
+ # Call the handler with appropriate parameters based on its signature
249
+ try:
250
+ sig = inspect.signature(handler)
251
+ handler_params = list(sig.parameters.keys())
252
+
253
+ if inspect.iscoroutinefunction(handler):
254
+ if uri_params:
255
+ # Use URI parameters for templated resources
256
+ content = await handler(**uri_params)
257
+ elif handler_params:
258
+ # Handler expects parameters, pass the full params dict
259
+ content = await handler(params)
260
+ else:
261
+ # Handler expects no parameters
262
+ content = await handler()
263
+ else:
264
+ if uri_params:
265
+ # Use URI parameters for templated resources
266
+ content = handler(**uri_params)
267
+ elif handler_params:
268
+ # Handler expects parameters, pass the full params dict
269
+ content = handler(params)
270
+ else:
271
+ # Handler expects no parameters
272
+ content = handler()
273
+ except TypeError as e:
274
+ # Handle parameter mismatch errors
275
+ raise MCPError(-32603, f"Handler parameter error: {str(e)}")
276
+
277
+ # Determine content type
278
+ metadata = self.resource_metadata[template_uri]
279
+ mime_type = metadata.mimeType or "text/plain"
280
+
281
+ return {"contents": [{"uri": uri, "mimeType": mime_type, "text": str(content)}]}
282
+
283
+ async def _handle_list_tools(self, params: Dict[str, Any]) -> Dict[str, Any]:
284
+ """Handle tools/list request"""
285
+ tools = []
286
+ for name, metadata in self.tool_metadata.items():
287
+ tools.append(asdict(metadata))
288
+ return {"tools": tools}
289
+
290
+ async def _handle_call_tool(self, params: Dict[str, Any]) -> Dict[str, Any]:
291
+ """Handle tools/call request"""
292
+ name = params.get("name")
293
+ arguments = params.get("arguments", {})
294
+
295
+ if not name or name not in self.tools:
296
+ raise MCPError(-32602, f"Tool not found: {name}")
297
+
298
+ handler = self.tools[name]
299
+
300
+ # Call the tool handler
301
+ if inspect.iscoroutinefunction(handler):
302
+ result = await handler(**arguments)
303
+ else:
304
+ result = handler(**arguments)
305
+
306
+ return {"content": [{"type": "text", "text": str(result)}]}
307
+
308
+ async def _handle_list_prompts(self, params: Dict[str, Any]) -> Dict[str, Any]:
309
+ """Handle prompts/list request"""
310
+ prompts = []
311
+ for name, metadata in self.prompt_metadata.items():
312
+ prompts.append(asdict(metadata))
313
+ return {"prompts": prompts}
314
+
315
+ async def _handle_get_prompt(self, params: Dict[str, Any]) -> Dict[str, Any]:
316
+ """Handle prompts/get request"""
317
+ name = params.get("name")
318
+ arguments = params.get("arguments", {})
319
+
320
+ if not name or name not in self.prompts:
321
+ raise MCPError(-32602, f"Prompt not found: {name}")
322
+
323
+ handler = self.prompts[name]
324
+
325
+ # Call the prompt handler
326
+ if inspect.iscoroutinefunction(handler):
327
+ result = await handler(**arguments)
328
+ else:
329
+ result = handler(**arguments)
330
+
331
+ # Ensure result is in the expected format
332
+ if isinstance(result, str):
333
+ messages = [{"role": "user", "content": {"type": "text", "text": result}}]
334
+ elif isinstance(result, list):
335
+ messages = result
336
+ else:
337
+ messages = [{"role": "user", "content": {"type": "text", "text": str(result)}}]
338
+
339
+ return {"description": self.prompt_metadata[name].description, "messages": messages}
340
+
341
+
342
+ class MCPApp:
343
+ """MCP application wrapper for Robyn"""
344
+
345
+ def __init__(self, robyn_app):
346
+ self.app = robyn_app
347
+ self.handler = MCPHandler()
348
+ self._setup_routes()
349
+
350
+ def _setup_routes(self):
351
+ """Setup MCP routes on the Robyn app"""
352
+
353
+ @self.app.post("/mcp")
354
+ async def handle_mcp_request(request):
355
+ """Handle MCP JSON-RPC requests"""
356
+ try:
357
+ # Parse JSON request - try multiple approaches
358
+ try:
359
+ request_data = request.json()
360
+ except (ValueError, TypeError, AttributeError):
361
+ # Fallback to parsing body as string
362
+ body = request.body
363
+ if isinstance(body, str):
364
+ request_data = json.loads(body)
365
+ else:
366
+ request_data = json.loads(body.decode("utf-8"))
367
+
368
+ # Handle case where request.json() returns a string instead of dict
369
+ if isinstance(request_data, str):
370
+ request_data = json.loads(request_data)
371
+
372
+ # Handle the request
373
+ response = await self.handler.handle_request(request_data)
374
+
375
+ return response
376
+
377
+ except json.JSONDecodeError:
378
+ return {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}}
379
+ except Exception as e:
380
+ logger.exception("Error in MCP request handler")
381
+ return {"jsonrpc": "2.0", "id": None, "error": {"code": -32603, "message": "Internal error", "data": str(e)}}
382
+
383
+ def resource(self, uri: str = None, name: str = None, description: Optional[str] = None, mime_type: Optional[str] = None):
384
+ """
385
+ Decorator to register an MCP resource
386
+
387
+ Args:
388
+ uri: Resource URI template (e.g., "echo://{message}")
389
+ name: Human-readable name (auto-generated if not provided)
390
+ description: Resource description (auto-generated if not provided)
391
+ mime_type: MIME type (defaults to "text/plain")
392
+
393
+ Example:
394
+ @app.mcp.resource("echo://{message}")
395
+ def echo_resource(message: str) -> str:
396
+ return f"Resource echo: {message}"
397
+ """
398
+
399
+ def decorator(func: Callable):
400
+ # Auto-generate metadata if not provided
401
+ actual_uri = uri or f"function://{func.__name__}"
402
+ actual_name = name or func.__name__.replace("_", " ").title()
403
+ actual_description = description or func.__doc__ or f"Resource: {actual_name}"
404
+ actual_mime_type = mime_type or "text/plain"
405
+
406
+ self.handler.register_resource(actual_uri, actual_name, func, actual_description, actual_mime_type)
407
+ return func
408
+
409
+ return decorator
410
+
411
+ def tool(self, name: str = None, description: str = None, input_schema: Dict[str, Any] = None):
412
+ """
413
+ Decorator to register an MCP tool
414
+
415
+ Args:
416
+ name: Tool name (defaults to function name)
417
+ description: Tool description (auto-generated if not provided)
418
+ input_schema: JSON schema for inputs (auto-generated if not provided)
419
+
420
+ Example:
421
+ @app.mcp.tool()
422
+ def echo_tool(message: str) -> str:
423
+ return f"Tool echo: {message}"
424
+ """
425
+
426
+ def decorator(func: Callable):
427
+ # Auto-generate metadata if not provided
428
+ actual_name = name or func.__name__
429
+ actual_description = description or func.__doc__ or f"Tool: {func.__name__}"
430
+ actual_schema = input_schema or _generate_schema_from_function(func)
431
+
432
+ self.handler.register_tool(actual_name, func, actual_description, actual_schema)
433
+ return func
434
+
435
+ return decorator
436
+
437
+ def prompt(self, name: str = None, description: str = None, arguments: Optional[List[Dict[str, Any]]] = None):
438
+ """
439
+ Decorator to register an MCP prompt
440
+
441
+ Args:
442
+ name: Prompt name (defaults to function name)
443
+ description: Prompt description (auto-generated if not provided)
444
+ arguments: Prompt arguments (auto-generated if not provided)
445
+
446
+ Example:
447
+ @app.mcp.prompt()
448
+ def echo_prompt(message: str) -> str:
449
+ return f"Please process this message: {message}"
450
+ """
451
+
452
+ def decorator(func: Callable):
453
+ # Auto-generate metadata if not provided
454
+ actual_name = name or func.__name__
455
+ actual_description = description or func.__doc__ or f"Prompt: {func.__name__}"
456
+ actual_arguments = arguments or _generate_prompt_args_from_function(func)
457
+
458
+ self.handler.register_prompt(actual_name, func, actual_description, actual_arguments)
459
+ return func
460
+
461
+ return decorator