remdb 0.3.146__py3-none-any.whl → 0.3.181__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.

Potentially problematic release.


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

Files changed (57) hide show
  1. rem/agentic/agents/__init__.py +16 -0
  2. rem/agentic/agents/agent_manager.py +311 -0
  3. rem/agentic/context.py +81 -3
  4. rem/agentic/context_builder.py +36 -9
  5. rem/agentic/mcp/tool_wrapper.py +43 -14
  6. rem/agentic/providers/pydantic_ai.py +76 -34
  7. rem/agentic/schema.py +4 -3
  8. rem/agentic/tools/rem_tools.py +11 -0
  9. rem/api/deps.py +3 -5
  10. rem/api/main.py +22 -3
  11. rem/api/mcp_router/resources.py +75 -14
  12. rem/api/mcp_router/server.py +28 -23
  13. rem/api/mcp_router/tools.py +177 -2
  14. rem/api/middleware/tracking.py +5 -5
  15. rem/api/routers/auth.py +352 -6
  16. rem/api/routers/chat/completions.py +5 -3
  17. rem/api/routers/chat/streaming.py +95 -22
  18. rem/api/routers/messages.py +24 -15
  19. rem/auth/__init__.py +13 -3
  20. rem/auth/jwt.py +352 -0
  21. rem/auth/middleware.py +70 -30
  22. rem/auth/providers/__init__.py +4 -1
  23. rem/auth/providers/email.py +215 -0
  24. rem/cli/commands/ask.py +1 -1
  25. rem/cli/commands/db.py +118 -54
  26. rem/models/entities/__init__.py +4 -0
  27. rem/models/entities/ontology.py +93 -101
  28. rem/models/entities/subscriber.py +175 -0
  29. rem/models/entities/user.py +1 -0
  30. rem/schemas/agents/core/agent-builder.yaml +235 -0
  31. rem/services/__init__.py +3 -1
  32. rem/services/content/service.py +4 -3
  33. rem/services/email/__init__.py +10 -0
  34. rem/services/email/service.py +522 -0
  35. rem/services/email/templates.py +360 -0
  36. rem/services/embeddings/worker.py +26 -12
  37. rem/services/postgres/README.md +38 -0
  38. rem/services/postgres/diff_service.py +19 -3
  39. rem/services/postgres/pydantic_to_sqlalchemy.py +37 -2
  40. rem/services/postgres/register_type.py +1 -1
  41. rem/services/postgres/repository.py +37 -25
  42. rem/services/postgres/schema_generator.py +5 -5
  43. rem/services/postgres/sql_builder.py +6 -5
  44. rem/services/session/compression.py +113 -50
  45. rem/services/session/reload.py +14 -7
  46. rem/services/user_service.py +41 -9
  47. rem/settings.py +182 -1
  48. rem/sql/background_indexes.sql +5 -0
  49. rem/sql/migrations/001_install.sql +33 -4
  50. rem/sql/migrations/002_install_models.sql +204 -186
  51. rem/sql/migrations/005_schema_update.sql +145 -0
  52. rem/utils/model_helpers.py +101 -0
  53. rem/utils/schema_loader.py +45 -7
  54. {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/METADATA +1 -1
  55. {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/RECORD +57 -48
  56. {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/WHEEL +0 -0
  57. {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/entry_points.txt +0 -0
@@ -149,12 +149,23 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
149
149
  parts = re.sub(r'_+', '_', parts).strip('_') # Clean up multiple underscores
150
150
  func_name = f"get_{parts}"
151
151
 
152
+ # For parameterized URIs, append _by_{params} to avoid naming conflicts
153
+ # e.g., rem://agents/{name} -> get_rem_agents_by_name (distinct from get_rem_agents)
154
+ if template_vars:
155
+ param_suffix = "_by_" + "_".join(template_vars)
156
+ func_name = f"{func_name}{param_suffix}"
157
+
152
158
  # Build description including parameter info
153
159
  description = usage or f"Fetch {uri} resource"
154
160
  if template_vars:
155
161
  param_desc = ", ".join(template_vars)
156
162
  description = f"{description}\n\nParameters: {param_desc}"
157
163
 
164
+ # Capture mcp_server reference at tool creation time (for closure)
165
+ # This ensures the correct server is used even if called later
166
+ _captured_mcp_server = mcp_server
167
+ _captured_uri = uri # Also capture URI for consistent logging
168
+
158
169
  if template_vars:
159
170
  # Template URI -> create parameterized tool
160
171
  async def wrapper(**kwargs: Any) -> str:
@@ -162,13 +173,17 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
162
173
  import asyncio
163
174
  import inspect
164
175
 
176
+ logger.debug(f"Resource tool invoked: uri={_captured_uri}, kwargs={kwargs}, mcp_server={'set' if _captured_mcp_server else 'None'}")
177
+
165
178
  # Try to resolve from MCP server's resource templates first
166
- if mcp_server is not None:
179
+ if _captured_mcp_server is not None:
167
180
  try:
168
181
  # Get resource templates from MCP server
169
- templates = await mcp_server.get_resource_templates()
170
- if uri in templates:
171
- template = templates[uri]
182
+ templates = await _captured_mcp_server.get_resource_templates()
183
+ logger.debug(f"MCP server templates: {list(templates.keys())}")
184
+ if _captured_uri in templates:
185
+ template = templates[_captured_uri]
186
+ logger.debug(f"Found template for {_captured_uri}, calling fn with kwargs={kwargs}")
172
187
  # Call the template's underlying function directly
173
188
  # The fn expects the template variables as kwargs
174
189
  fn_result = template.fn(**kwargs)
@@ -178,17 +193,22 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
178
193
  if isinstance(fn_result, str):
179
194
  return fn_result
180
195
  return json.dumps(fn_result, indent=2)
196
+ else:
197
+ logger.warning(f"Template {_captured_uri} not found in MCP server templates: {list(templates.keys())}")
181
198
  except Exception as e:
182
- logger.warning(f"Failed to resolve resource {uri} from MCP server: {e}")
199
+ logger.warning(f"Failed to resolve resource {_captured_uri} from MCP server: {e}", exc_info=True)
200
+ else:
201
+ logger.warning(f"No MCP server provided for resource tool {_captured_uri}, using fallback")
183
202
 
184
203
  # Fallback: substitute template variables and use load_resource
185
- resolved_uri = uri
204
+ resolved_uri = _captured_uri
186
205
  for var in template_vars:
187
206
  if var in kwargs:
188
207
  resolved_uri = resolved_uri.replace(f"{{{var}}}", str(kwargs[var]))
189
208
  else:
190
209
  return json.dumps({"error": f"Missing required parameter: {var}"})
191
210
 
211
+ logger.debug(f"Using fallback load_resource for resolved URI: {resolved_uri}")
192
212
  from rem.api.mcp_router.resources import load_resource
193
213
  result = await load_resource(resolved_uri)
194
214
  if isinstance(result, str):
@@ -202,7 +222,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
202
222
  wrapper.__annotations__ = {var: str for var in template_vars}
203
223
  wrapper.__annotations__['return'] = str
204
224
 
205
- logger.info(f"Built parameterized resource tool: {func_name} (uri: {uri}, params: {template_vars})")
225
+ logger.info(f"Built parameterized resource tool: {func_name} (uri: {uri}, params: {template_vars}, mcp_server={'provided' if mcp_server else 'None'})")
206
226
  else:
207
227
  # Concrete URI -> no-param tool
208
228
  async def wrapper(**kwargs: Any) -> str:
@@ -213,12 +233,16 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
213
233
  if kwargs:
214
234
  logger.warning(f"Resource tool {func_name} called with unexpected kwargs: {list(kwargs.keys())}")
215
235
 
236
+ logger.debug(f"Concrete resource tool invoked: uri={_captured_uri}, mcp_server={'set' if _captured_mcp_server else 'None'}")
237
+
216
238
  # Try to resolve from MCP server's resources first
217
- if mcp_server is not None:
239
+ if _captured_mcp_server is not None:
218
240
  try:
219
- resources = await mcp_server.get_resources()
220
- if uri in resources:
221
- resource = resources[uri]
241
+ resources = await _captured_mcp_server.get_resources()
242
+ logger.debug(f"MCP server resources: {list(resources.keys())}")
243
+ if _captured_uri in resources:
244
+ resource = resources[_captured_uri]
245
+ logger.debug(f"Found resource for {_captured_uri}")
222
246
  # Call the resource's underlying function
223
247
  fn_result = resource.fn()
224
248
  if inspect.iscoroutine(fn_result):
@@ -226,12 +250,17 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
226
250
  if isinstance(fn_result, str):
227
251
  return fn_result
228
252
  return json.dumps(fn_result, indent=2)
253
+ else:
254
+ logger.warning(f"Resource {_captured_uri} not found in MCP server resources: {list(resources.keys())}")
229
255
  except Exception as e:
230
- logger.warning(f"Failed to resolve resource {uri} from MCP server: {e}")
256
+ logger.warning(f"Failed to resolve resource {_captured_uri} from MCP server: {e}", exc_info=True)
257
+ else:
258
+ logger.warning(f"No MCP server provided for resource tool {_captured_uri}, using fallback")
231
259
 
232
260
  # Fallback to load_resource
261
+ logger.debug(f"Using fallback load_resource for URI: {_captured_uri}")
233
262
  from rem.api.mcp_router.resources import load_resource
234
- result = await load_resource(uri)
263
+ result = await load_resource(_captured_uri)
235
264
  if isinstance(result, str):
236
265
  return result
237
266
  return json.dumps(result, indent=2)
@@ -239,6 +268,6 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
239
268
  wrapper.__name__ = func_name
240
269
  wrapper.__doc__ = description
241
270
 
242
- logger.info(f"Built resource tool: {func_name} (uri: {uri})")
271
+ logger.info(f"Built resource tool: {func_name} (uri: {uri}, mcp_server={'provided' if mcp_server else 'None'})")
243
272
 
244
273
  return Tool(wrapper)
@@ -96,6 +96,35 @@ TODO:
96
96
 
97
97
  Priority: HIGH (blocks production scaling beyond 50 req/sec)
98
98
 
99
+ 4. Response Format Control (structured_output enhancement):
100
+ - Current: structured_output is bool (True=strict schema, False=free-form text)
101
+ - Missing: OpenAI JSON mode (valid JSON without strict schema enforcement)
102
+ - Missing: Completions API support (some models only support completions, not chat)
103
+
104
+ Proposed schema field values for `structured_output`:
105
+ - True (default): Strict structured output using provider's native schema support
106
+ - False: Free-form text response (properties converted to prompt guidance)
107
+ - "json": JSON mode - ensures valid JSON but no schema enforcement
108
+ (OpenAI: response_format={"type": "json_object"})
109
+ - "text": Explicit free-form text (alias for False)
110
+
111
+ Implementation:
112
+ a) Update AgentSchemaMetadata.structured_output type:
113
+ structured_output: bool | Literal["json", "text"] = True
114
+ b) In create_agent(), handle each mode:
115
+ - True: Use output_type with Pydantic model (current behavior)
116
+ - False/"text": Convert properties to prompt guidance (current behavior)
117
+ - "json": Use provider's JSON mode without strict schema
118
+ c) Provider-specific JSON mode:
119
+ - OpenAI: model_settings={"response_format": {"type": "json_object"}}
120
+ - Anthropic: Not supported natively, use prompt guidance
121
+ - Others: Fallback to prompt guidance with JSON instruction
122
+
123
+ Related: Some providers (Cerebras) have completions-only models where
124
+ structured output isn't available. Consider model capability detection.
125
+
126
+ Priority: MEDIUM (enables more flexible output control)
127
+
99
128
  Example Agent Schema:
100
129
  {
101
130
  "type": "object",
@@ -553,58 +582,70 @@ async def create_agent(
553
582
  if agent_schema:
554
583
  system_prompt = get_system_prompt(agent_schema)
555
584
  metadata = get_metadata(agent_schema)
556
- mcp_server_configs = [s.model_dump() for s in metadata.mcp_servers] if hasattr(metadata, 'mcp_servers') and metadata.mcp_servers else []
557
585
  resource_configs = metadata.resources if hasattr(metadata, 'resources') else []
558
586
 
587
+ # DEPRECATED: mcp_servers in agent schemas is ignored
588
+ # MCP servers are now always auto-detected at the application level
589
+ if hasattr(metadata, 'mcp_servers') and metadata.mcp_servers:
590
+ logger.warning(
591
+ "DEPRECATED: mcp_servers in agent schema is ignored. "
592
+ "MCP servers are auto-detected from tools.mcp_server module. "
593
+ "Remove mcp_servers from your agent schema."
594
+ )
595
+
559
596
  if metadata.system_prompt:
560
597
  logger.debug("Using custom system_prompt from json_schema_extra")
561
598
  else:
562
599
  system_prompt = ""
563
600
  metadata = None
564
- mcp_server_configs = []
565
601
  resource_configs = []
566
602
 
567
- # Auto-detect local MCP server if not explicitly configured
568
- # This makes mcp_servers config optional - agents get tools automatically
603
+ # Auto-detect MCP server at application level
604
+ # Convention: tools/mcp_server.py exports `mcp` FastMCP instance
605
+ # Falls back to REM's built-in MCP server if no local server found
606
+ import importlib
607
+ import os
608
+ import sys
609
+
610
+ # Ensure current working directory is in sys.path for local imports
611
+ cwd = os.getcwd()
612
+ if cwd not in sys.path:
613
+ sys.path.insert(0, cwd)
614
+
615
+ mcp_server_configs = []
616
+ auto_detect_modules = [
617
+ "tools.mcp_server", # Convention: tools/mcp_server.py
618
+ "mcp_server", # Alternative: mcp_server.py in root
619
+ ]
620
+ for module_path in auto_detect_modules:
621
+ try:
622
+ mcp_module = importlib.import_module(module_path)
623
+ if hasattr(mcp_module, "mcp"):
624
+ logger.info(f"Auto-detected local MCP server: {module_path}")
625
+ mcp_server_configs = [{"type": "local", "module": module_path, "id": "auto-detected"}]
626
+ break
627
+ except ImportError as e:
628
+ logger.debug(f"MCP server auto-detect: {module_path} not found ({e})")
629
+ continue
630
+ except Exception as e:
631
+ logger.warning(f"MCP server auto-detect: {module_path} failed to load: {e}")
632
+ continue
633
+
634
+ # Fall back to REM's default MCP server if no local server found
569
635
  if not mcp_server_configs:
570
- import importlib
571
- import os
572
- import sys
573
-
574
- # Ensure current working directory is in sys.path for local imports
575
- cwd = os.getcwd()
576
- if cwd not in sys.path:
577
- sys.path.insert(0, cwd)
578
-
579
- # Try common local MCP server module paths first
580
- auto_detect_modules = [
581
- "tools.mcp_server", # Convention: tools/mcp_server.py
582
- "mcp_server", # Alternative: mcp_server.py in root
583
- ]
584
- for module_path in auto_detect_modules:
585
- try:
586
- mcp_module = importlib.import_module(module_path)
587
- if hasattr(mcp_module, "mcp"):
588
- logger.info(f"Auto-detected local MCP server: {module_path}")
589
- mcp_server_configs = [{"type": "local", "module": module_path, "id": "auto-detected"}]
590
- break
591
- except ImportError:
592
- continue
593
-
594
- # Fall back to REM's default MCP server if no local server found
595
- if not mcp_server_configs:
596
- logger.debug("No local MCP server found, using REM default")
597
- mcp_server_configs = [{"type": "local", "module": "rem.mcp_server", "id": "rem"}]
636
+ logger.info("No local MCP server found, using REM default (rem.mcp_server)")
637
+ mcp_server_configs = [{"type": "local", "module": "rem.mcp_server", "id": "rem"}]
598
638
 
599
639
  # Extract temperature and max_iterations from schema metadata (with fallback to settings defaults)
600
640
  if metadata:
601
641
  temperature = metadata.override_temperature if metadata.override_temperature is not None else settings.llm.default_temperature
602
642
  max_iterations = metadata.override_max_iterations if metadata.override_max_iterations is not None else settings.llm.default_max_iterations
603
- use_structured_output = metadata.structured_output
643
+ # Use schema-level structured_output if set, otherwise fall back to global setting
644
+ use_structured_output = metadata.structured_output if metadata.structured_output is not None else settings.llm.default_structured_output
604
645
  else:
605
646
  temperature = settings.llm.default_temperature
606
647
  max_iterations = settings.llm.default_max_iterations
607
- use_structured_output = True
648
+ use_structured_output = settings.llm.default_structured_output
608
649
 
609
650
  # Build list of tools - start with built-in tools
610
651
  tools = _get_builtin_tools()
@@ -727,6 +768,7 @@ async def create_agent(
727
768
 
728
769
  # Create tools from collected resource URIs
729
770
  # Pass the loaded MCP server so resources can be resolved from it
771
+ logger.info(f"Creating {len(resource_uris)} resource tools with mcp_server={'set' if loaded_mcp_server else 'None'}")
730
772
  for uri, usage in resource_uris:
731
773
  resource_tool = create_resource_tool(uri, usage, mcp_server=loaded_mcp_server)
732
774
  tools.append(resource_tool)
rem/agentic/schema.py CHANGED
@@ -215,12 +215,13 @@ class AgentSchemaMetadata(BaseModel):
215
215
  )
216
216
 
217
217
  # Structured output toggle
218
- structured_output: bool = Field(
219
- default=True,
218
+ structured_output: bool | None = Field(
219
+ default=None,
220
220
  description=(
221
221
  "Whether to enforce structured JSON output. "
222
222
  "When False, the agent produces free-form text and schema properties "
223
- "are converted to prompt guidance instead. Default: True (JSON output)."
223
+ "are converted to prompt guidance instead. "
224
+ "Default: None (uses LLM__DEFAULT_STRUCTURED_OUTPUT setting, which defaults to False)."
224
225
  ),
225
226
  )
226
227
 
@@ -3,6 +3,17 @@ REM tools for agent execution (CLI and API compatible).
3
3
 
4
4
  These tools work in both CLI and API contexts by initializing services on-demand.
5
5
  They wrap the service layer directly, not MCP tools.
6
+
7
+ Core tables (always available):
8
+ - resources: Documents, content chunks, artifacts
9
+ - moments: Temporal narratives extracted from resources (usually user-specific)
10
+ - ontologies: Domain entities with semantic links for further lookups (like a wiki)
11
+
12
+ Other tables (may vary by deployment):
13
+ - users, sessions, messages, files, schemas, feedbacks
14
+
15
+ Note: Not all tables are populated in all systems. Use FUZZY or SEARCH
16
+ to discover what data exists before assuming specific tables have content.
6
17
  """
7
18
 
8
19
  from typing import Any, Literal, cast
rem/api/deps.py CHANGED
@@ -147,7 +147,6 @@ def is_admin(user: dict | None) -> bool:
147
147
  async def get_user_filter(
148
148
  request: Request,
149
149
  x_user_id: str | None = None,
150
- x_tenant_id: str = "default",
151
150
  ) -> dict[str, Any]:
152
151
  """
153
152
  Get user-scoped filter dict for database queries.
@@ -158,7 +157,6 @@ async def get_user_filter(
158
157
  Args:
159
158
  request: FastAPI request
160
159
  x_user_id: Optional user_id filter (admin only for cross-user)
161
- x_tenant_id: Tenant ID for multi-tenancy
162
160
 
163
161
  Returns:
164
162
  Filter dict with appropriate user_id constraint
@@ -169,7 +167,7 @@ async def get_user_filter(
169
167
  return await repo.find(filters)
170
168
  """
171
169
  user = get_current_user(request)
172
- filters: dict[str, Any] = {"tenant_id": x_tenant_id}
170
+ filters: dict[str, Any] = {}
173
171
 
174
172
  if is_admin(user):
175
173
  # Admin can filter by any user or see all
@@ -185,8 +183,8 @@ async def get_user_filter(
185
183
  f"User {user.get('email')} attempted to filter by user_id={x_user_id}"
186
184
  )
187
185
  else:
188
- # Anonymous: could use anonymous tracking ID or restrict access
189
- # For now, anonymous can't access user-scoped data
186
+ # Anonymous: use anonymous tracking ID
187
+ # Note: user_id should come from JWT, not from parameters
190
188
  anon_id = getattr(request.state, "anon_id", None)
191
189
  if anon_id:
192
190
  filters["user_id"] = f"anon:{anon_id}"
rem/api/main.py CHANGED
@@ -149,19 +149,38 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
149
149
  client_host = request.client.host if request.client else "unknown"
150
150
  user_agent = request.headers.get('user-agent', 'unknown')[:100]
151
151
 
152
+ # Extract auth info for logging (first 8 chars of token for debugging)
153
+ auth_header = request.headers.get('authorization', '')
154
+ auth_preview = ""
155
+ if auth_header.startswith('Bearer '):
156
+ token = auth_header[7:]
157
+ auth_preview = f"Bearer {token[:8]}..." if len(token) > 8 else f"Bearer {token}"
158
+
152
159
  # Process request
153
160
  response = await call_next(request)
154
161
 
162
+ # Extract user info set by auth middleware (after processing)
163
+ user = getattr(request.state, "user", None)
164
+ user_id = user.get("id", "none")[:12] if user else "anon"
165
+ user_email = user.get("email", "") if user else ""
166
+
155
167
  # Determine log level based on path AND response status
156
168
  duration_ms = (time.time() - start_time) * 1000
157
169
  use_debug = self._should_log_at_debug(path, response.status_code)
158
170
  log_fn = logger.debug if use_debug else logger.info
159
171
 
160
- # Log request and response together
172
+ # Build user info string
173
+ user_info = f"user={user_id}"
174
+ if user_email:
175
+ user_info += f" ({user_email})"
176
+ if auth_preview:
177
+ user_info += f" | auth={auth_preview}"
178
+
179
+ # Log request and response together with auth info
161
180
  log_fn(
162
181
  f"→ REQUEST: {request.method} {path} | "
163
182
  f"Client: {client_host} | "
164
- f"User-Agent: {user_agent}"
183
+ f"{user_info}"
165
184
  )
166
185
  log_fn(
167
186
  f"← RESPONSE: {request.method} {path} | "
@@ -304,7 +323,7 @@ def create_app() -> FastAPI:
304
323
  app.add_middleware(
305
324
  AuthMiddleware,
306
325
  protected_paths=["/api/v1"],
307
- excluded_paths=["/api/auth", "/api/dev", "/api/v1/mcp/auth"],
326
+ excluded_paths=["/api/auth", "/api/dev", "/api/v1/mcp/auth", "/api/v1/slack"],
308
327
  # Allow anonymous when auth is disabled, otherwise use setting
309
328
  allow_anonymous=(not settings.auth.enabled) or settings.auth.allow_anonymous,
310
329
  # MCP requires auth only when auth is fully enabled
@@ -349,6 +349,53 @@ def register_agent_resources(mcp: FastMCP):
349
349
  except Exception as e:
350
350
  return f"# Available Agents\n\nError listing agents: {e}"
351
351
 
352
+ @mcp.resource("rem://agents/{agent_name}")
353
+ def get_agent_schema(agent_name: str) -> str:
354
+ """
355
+ Get a specific agent schema by name.
356
+
357
+ Args:
358
+ agent_name: Name of the agent (e.g., "ask_rem", "agent-builder")
359
+
360
+ Returns:
361
+ Full agent schema as YAML string, or error message if not found.
362
+ """
363
+ import importlib.resources
364
+ import yaml
365
+ from pathlib import Path
366
+
367
+ try:
368
+ # Find packaged agent schemas
369
+ agents_ref = importlib.resources.files("rem") / "schemas" / "agents"
370
+ agents_dir = Path(str(agents_ref))
371
+
372
+ if not agents_dir.exists():
373
+ return f"# Agent Not Found\n\nNo agent schemas directory found."
374
+
375
+ # Search for agent file (try multiple extensions)
376
+ for ext in [".yaml", ".yml", ".json"]:
377
+ # Try exact match first
378
+ agent_file = agents_dir / f"{agent_name}{ext}"
379
+ if agent_file.exists():
380
+ with open(agent_file, "r") as f:
381
+ content = f.read()
382
+ return f"# Agent Schema: {agent_name}\n\n```yaml\n{content}\n```"
383
+
384
+ # Try recursive search
385
+ matches = list(agents_dir.rglob(f"{agent_name}{ext}"))
386
+ if matches:
387
+ with open(matches[0], "r") as f:
388
+ content = f.read()
389
+ return f"# Agent Schema: {agent_name}\n\n```yaml\n{content}\n```"
390
+
391
+ # Not found - list available agents
392
+ available = [f.stem for f in agents_dir.rglob("*.yaml")] + \
393
+ [f.stem for f in agents_dir.rglob("*.yml")]
394
+ return f"# Agent Not Found\n\nAgent '{agent_name}' not found.\n\nAvailable agents: {', '.join(sorted(set(available)))}"
395
+
396
+ except Exception as e:
397
+ return f"# Error\n\nError loading agent '{agent_name}': {e}"
398
+
352
399
 
353
400
  def register_file_resources(mcp: FastMCP):
354
401
  """
@@ -501,10 +548,11 @@ async def load_resource(uri: str) -> dict | str:
501
548
  Load an MCP resource by URI.
502
549
 
503
550
  This function is called by the read_resource tool to dispatch to
504
- registered resource handlers.
551
+ registered resource handlers. Supports both regular resources and
552
+ parameterized resource templates (e.g., rem://agents/{agent_name}).
505
553
 
506
554
  Args:
507
- uri: Resource URI (e.g., "rem://schemas", "rem://status")
555
+ uri: Resource URI (e.g., "rem://agents", "rem://agents/ask_rem", "rem://status")
508
556
 
509
557
  Returns:
510
558
  Resource data (dict or string)
@@ -512,9 +560,10 @@ async def load_resource(uri: str) -> dict | str:
512
560
  Raises:
513
561
  ValueError: If URI is invalid or resource not found
514
562
  """
515
- # Create temporary MCP instance with resources
563
+ import inspect
516
564
  from fastmcp import FastMCP
517
565
 
566
+ # Create temporary MCP instance with resources
518
567
  mcp = FastMCP(name="temp")
519
568
 
520
569
  # Register all resources
@@ -523,14 +572,26 @@ async def load_resource(uri: str) -> dict | str:
523
572
  register_file_resources(mcp)
524
573
  register_status_resources(mcp)
525
574
 
526
- # Get resource handlers from MCP internal registry
527
- # FastMCP stores resources in a dict by URI
528
- if hasattr(mcp, "_resources"):
529
- if uri in mcp._resources:
530
- handler = mcp._resources[uri]
531
- if callable(handler):
532
- result = handler()
533
- return result if result else {"error": "Resource returned None"}
534
-
535
- # If not found, raise error
536
- raise ValueError(f"Resource not found: {uri}. Available resources: {list(mcp._resources.keys()) if hasattr(mcp, '_resources') else 'unknown'}")
575
+ # 1. Try exact match in regular resources
576
+ resources = await mcp.get_resources()
577
+ if uri in resources:
578
+ resource = resources[uri]
579
+ result = resource.fn()
580
+ if inspect.iscoroutine(result):
581
+ result = await result
582
+ return result if result else {"error": "Resource returned None"}
583
+
584
+ # 2. Try matching against parameterized resource templates
585
+ templates = await mcp.get_resource_templates()
586
+ for template_uri, template in templates.items():
587
+ params = template.matches(uri)
588
+ if params is not None:
589
+ # Template matched - call function with extracted parameters
590
+ result = template.fn(**params)
591
+ if inspect.iscoroutine(result):
592
+ result = await result
593
+ return result if result else {"error": "Resource returned None"}
594
+
595
+ # 3. Not found - include both resources and templates in error
596
+ available = list(resources.keys()) + list(templates.keys())
597
+ raise ValueError(f"Resource not found: {uri}. Available resources: {available}")
@@ -1,7 +1,7 @@
1
1
  """
2
2
  MCP server creation and configuration for REM.
3
3
 
4
- Design Pattern
4
+ Design Pattern
5
5
  1. Create FastMCP server with tools and resources
6
6
  2. Register tools using @mcp.tool() decorator
7
7
  3. Register resources using resource registration functions
@@ -20,10 +20,30 @@ FastMCP Features:
20
20
  """
21
21
 
22
22
  import importlib.metadata
23
+ from functools import wraps
23
24
 
24
25
  from fastmcp import FastMCP
26
+ from loguru import logger
25
27
 
26
28
  from ...settings import settings
29
+ from .prompts import register_prompts
30
+ from .resources import (
31
+ register_agent_resources,
32
+ register_file_resources,
33
+ register_schema_resources,
34
+ register_status_resources,
35
+ )
36
+ from .tools import (
37
+ ask_rem_agent,
38
+ get_schema,
39
+ ingest_into_rem,
40
+ list_schema,
41
+ read_resource,
42
+ register_metadata,
43
+ save_agent,
44
+ search_rem,
45
+ test_error_handling,
46
+ )
27
47
 
28
48
  # Get package version
29
49
  try:
@@ -174,28 +194,22 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
174
194
  ),
175
195
  )
176
196
 
177
- # Register REM tools
178
- from .tools import (
179
- ask_rem_agent,
180
- get_schema,
181
- ingest_into_rem,
182
- list_schema,
183
- read_resource,
184
- register_metadata,
185
- search_rem,
186
- )
187
-
197
+ # Register core REM tools
188
198
  mcp.tool()(search_rem)
189
199
  mcp.tool()(ask_rem_agent)
190
200
  mcp.tool()(read_resource)
191
201
  mcp.tool()(register_metadata)
192
202
  mcp.tool()(list_schema)
193
203
  mcp.tool()(get_schema)
204
+ mcp.tool()(save_agent)
205
+
206
+ # Register test tool only in development environment (not staging/production)
207
+ if settings.environment not in ("staging", "production"):
208
+ mcp.tool()(test_error_handling)
209
+ logger.debug("Registered test_error_handling tool (dev environment only)")
194
210
 
195
211
  # File ingestion tool (with local path support for local servers)
196
212
  # Wrap to inject is_local parameter
197
- from functools import wraps
198
-
199
213
  @wraps(ingest_into_rem)
200
214
  async def ingest_into_rem_wrapper(
201
215
  file_uri: str,
@@ -214,18 +228,9 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
214
228
  mcp.tool()(ingest_into_rem_wrapper)
215
229
 
216
230
  # Register prompts
217
- from .prompts import register_prompts
218
-
219
231
  register_prompts(mcp)
220
232
 
221
233
  # Register schema resources
222
- from .resources import (
223
- register_agent_resources,
224
- register_file_resources,
225
- register_schema_resources,
226
- register_status_resources,
227
- )
228
-
229
234
  register_schema_resources(mcp)
230
235
  register_agent_resources(mcp)
231
236
  register_file_resources(mcp)