remdb 0.3.171__py3-none-any.whl → 0.3.180__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.
- rem/agentic/mcp/tool_wrapper.py +37 -14
- rem/agentic/providers/pydantic_ai.py +77 -44
- rem/agentic/schema.py +4 -3
- rem/agentic/tools/rem_tools.py +11 -0
- rem/api/mcp_router/resources.py +75 -14
- rem/api/mcp_router/server.py +27 -24
- rem/api/mcp_router/tools.py +79 -0
- rem/api/routers/auth.py +8 -5
- rem/cli/commands/ask.py +1 -1
- rem/cli/commands/db.py +98 -44
- rem/models/entities/ontology.py +93 -101
- rem/services/email/service.py +11 -2
- rem/services/postgres/register_type.py +1 -1
- rem/settings.py +6 -0
- rem/sql/background_indexes.sql +5 -0
- rem/sql/migrations/001_install.sql +32 -3
- rem/sql/migrations/002_install_models.sql +186 -168
- rem/utils/model_helpers.py +101 -0
- {remdb-0.3.171.dist-info → remdb-0.3.180.dist-info}/METADATA +1 -1
- {remdb-0.3.171.dist-info → remdb-0.3.180.dist-info}/RECORD +22 -22
- {remdb-0.3.171.dist-info → remdb-0.3.180.dist-info}/WHEEL +0 -0
- {remdb-0.3.171.dist-info → remdb-0.3.180.dist-info}/entry_points.txt +0 -0
rem/agentic/mcp/tool_wrapper.py
CHANGED
|
@@ -161,6 +161,11 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
|
|
|
161
161
|
param_desc = ", ".join(template_vars)
|
|
162
162
|
description = f"{description}\n\nParameters: {param_desc}"
|
|
163
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
|
+
|
|
164
169
|
if template_vars:
|
|
165
170
|
# Template URI -> create parameterized tool
|
|
166
171
|
async def wrapper(**kwargs: Any) -> str:
|
|
@@ -168,13 +173,17 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
|
|
|
168
173
|
import asyncio
|
|
169
174
|
import inspect
|
|
170
175
|
|
|
176
|
+
logger.debug(f"Resource tool invoked: uri={_captured_uri}, kwargs={kwargs}, mcp_server={'set' if _captured_mcp_server else 'None'}")
|
|
177
|
+
|
|
171
178
|
# Try to resolve from MCP server's resource templates first
|
|
172
|
-
if
|
|
179
|
+
if _captured_mcp_server is not None:
|
|
173
180
|
try:
|
|
174
181
|
# Get resource templates from MCP server
|
|
175
|
-
templates = await
|
|
176
|
-
|
|
177
|
-
|
|
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}")
|
|
178
187
|
# Call the template's underlying function directly
|
|
179
188
|
# The fn expects the template variables as kwargs
|
|
180
189
|
fn_result = template.fn(**kwargs)
|
|
@@ -184,17 +193,22 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
|
|
|
184
193
|
if isinstance(fn_result, str):
|
|
185
194
|
return fn_result
|
|
186
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())}")
|
|
187
198
|
except Exception as e:
|
|
188
|
-
logger.warning(f"Failed to resolve resource {
|
|
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")
|
|
189
202
|
|
|
190
203
|
# Fallback: substitute template variables and use load_resource
|
|
191
|
-
resolved_uri =
|
|
204
|
+
resolved_uri = _captured_uri
|
|
192
205
|
for var in template_vars:
|
|
193
206
|
if var in kwargs:
|
|
194
207
|
resolved_uri = resolved_uri.replace(f"{{{var}}}", str(kwargs[var]))
|
|
195
208
|
else:
|
|
196
209
|
return json.dumps({"error": f"Missing required parameter: {var}"})
|
|
197
210
|
|
|
211
|
+
logger.debug(f"Using fallback load_resource for resolved URI: {resolved_uri}")
|
|
198
212
|
from rem.api.mcp_router.resources import load_resource
|
|
199
213
|
result = await load_resource(resolved_uri)
|
|
200
214
|
if isinstance(result, str):
|
|
@@ -208,7 +222,7 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
|
|
|
208
222
|
wrapper.__annotations__ = {var: str for var in template_vars}
|
|
209
223
|
wrapper.__annotations__['return'] = str
|
|
210
224
|
|
|
211
|
-
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'})")
|
|
212
226
|
else:
|
|
213
227
|
# Concrete URI -> no-param tool
|
|
214
228
|
async def wrapper(**kwargs: Any) -> str:
|
|
@@ -219,12 +233,16 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
|
|
|
219
233
|
if kwargs:
|
|
220
234
|
logger.warning(f"Resource tool {func_name} called with unexpected kwargs: {list(kwargs.keys())}")
|
|
221
235
|
|
|
236
|
+
logger.debug(f"Concrete resource tool invoked: uri={_captured_uri}, mcp_server={'set' if _captured_mcp_server else 'None'}")
|
|
237
|
+
|
|
222
238
|
# Try to resolve from MCP server's resources first
|
|
223
|
-
if
|
|
239
|
+
if _captured_mcp_server is not None:
|
|
224
240
|
try:
|
|
225
|
-
resources = await
|
|
226
|
-
|
|
227
|
-
|
|
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}")
|
|
228
246
|
# Call the resource's underlying function
|
|
229
247
|
fn_result = resource.fn()
|
|
230
248
|
if inspect.iscoroutine(fn_result):
|
|
@@ -232,12 +250,17 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
|
|
|
232
250
|
if isinstance(fn_result, str):
|
|
233
251
|
return fn_result
|
|
234
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())}")
|
|
235
255
|
except Exception as e:
|
|
236
|
-
logger.warning(f"Failed to resolve resource {
|
|
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")
|
|
237
259
|
|
|
238
260
|
# Fallback to load_resource
|
|
261
|
+
logger.debug(f"Using fallback load_resource for URI: {_captured_uri}")
|
|
239
262
|
from rem.api.mcp_router.resources import load_resource
|
|
240
|
-
result = await load_resource(
|
|
263
|
+
result = await load_resource(_captured_uri)
|
|
241
264
|
if isinstance(result, str):
|
|
242
265
|
return result
|
|
243
266
|
return json.dumps(result, indent=2)
|
|
@@ -245,6 +268,6 @@ def create_resource_tool(uri: str, usage: str = "", mcp_server: Any = None) -> T
|
|
|
245
268
|
wrapper.__name__ = func_name
|
|
246
269
|
wrapper.__doc__ = description
|
|
247
270
|
|
|
248
|
-
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'})")
|
|
249
272
|
|
|
250
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",
|
|
@@ -550,70 +579,73 @@ async def create_agent(
|
|
|
550
579
|
# Extract schema fields using typed helpers
|
|
551
580
|
from ..schema import get_system_prompt, get_metadata
|
|
552
581
|
|
|
553
|
-
# Track whether mcp_servers was explicitly configured (even if empty)
|
|
554
|
-
mcp_servers_explicitly_set = False
|
|
555
|
-
|
|
556
582
|
if agent_schema:
|
|
557
583
|
system_prompt = get_system_prompt(agent_schema)
|
|
558
584
|
metadata = get_metadata(agent_schema)
|
|
559
|
-
# Check if mcp_servers was explicitly set (could be empty list to disable)
|
|
560
|
-
if hasattr(metadata, 'mcp_servers') and metadata.mcp_servers is not None:
|
|
561
|
-
mcp_server_configs = [s.model_dump() for s in metadata.mcp_servers]
|
|
562
|
-
mcp_servers_explicitly_set = True
|
|
563
|
-
else:
|
|
564
|
-
mcp_server_configs = []
|
|
565
585
|
resource_configs = metadata.resources if hasattr(metadata, 'resources') else []
|
|
566
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
|
+
|
|
567
596
|
if metadata.system_prompt:
|
|
568
597
|
logger.debug("Using custom system_prompt from json_schema_extra")
|
|
569
598
|
else:
|
|
570
599
|
system_prompt = ""
|
|
571
600
|
metadata = None
|
|
572
|
-
mcp_server_configs = []
|
|
573
601
|
resource_configs = []
|
|
574
602
|
|
|
575
|
-
# Auto-detect
|
|
576
|
-
#
|
|
577
|
-
#
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
635
|
+
if not mcp_server_configs:
|
|
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"}]
|
|
607
638
|
|
|
608
639
|
# Extract temperature and max_iterations from schema metadata (with fallback to settings defaults)
|
|
609
640
|
if metadata:
|
|
610
641
|
temperature = metadata.override_temperature if metadata.override_temperature is not None else settings.llm.default_temperature
|
|
611
642
|
max_iterations = metadata.override_max_iterations if metadata.override_max_iterations is not None else settings.llm.default_max_iterations
|
|
612
|
-
|
|
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
|
|
613
645
|
else:
|
|
614
646
|
temperature = settings.llm.default_temperature
|
|
615
647
|
max_iterations = settings.llm.default_max_iterations
|
|
616
|
-
use_structured_output =
|
|
648
|
+
use_structured_output = settings.llm.default_structured_output
|
|
617
649
|
|
|
618
650
|
# Build list of tools - start with built-in tools
|
|
619
651
|
tools = _get_builtin_tools()
|
|
@@ -736,6 +768,7 @@ async def create_agent(
|
|
|
736
768
|
|
|
737
769
|
# Create tools from collected resource URIs
|
|
738
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'}")
|
|
739
772
|
for uri, usage in resource_uris:
|
|
740
773
|
resource_tool = create_resource_tool(uri, usage, mcp_server=loaded_mcp_server)
|
|
741
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=
|
|
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.
|
|
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
|
|
rem/agentic/tools/rem_tools.py
CHANGED
|
@@ -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/mcp_router/resources.py
CHANGED
|
@@ -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://
|
|
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
|
-
|
|
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
|
-
#
|
|
527
|
-
|
|
528
|
-
if
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
#
|
|
536
|
-
|
|
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}")
|
rem/api/mcp_router/server.py
CHANGED
|
@@ -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,18 +194,7 @@ 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
|
-
save_agent,
|
|
186
|
-
search_rem,
|
|
187
|
-
)
|
|
188
|
-
|
|
197
|
+
# Register core REM tools
|
|
189
198
|
mcp.tool()(search_rem)
|
|
190
199
|
mcp.tool()(ask_rem_agent)
|
|
191
200
|
mcp.tool()(read_resource)
|
|
@@ -194,10 +203,13 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
|
194
203
|
mcp.tool()(get_schema)
|
|
195
204
|
mcp.tool()(save_agent)
|
|
196
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)")
|
|
210
|
+
|
|
197
211
|
# File ingestion tool (with local path support for local servers)
|
|
198
212
|
# Wrap to inject is_local parameter
|
|
199
|
-
from functools import wraps
|
|
200
|
-
|
|
201
213
|
@wraps(ingest_into_rem)
|
|
202
214
|
async def ingest_into_rem_wrapper(
|
|
203
215
|
file_uri: str,
|
|
@@ -216,18 +228,9 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
|
216
228
|
mcp.tool()(ingest_into_rem_wrapper)
|
|
217
229
|
|
|
218
230
|
# Register prompts
|
|
219
|
-
from .prompts import register_prompts
|
|
220
|
-
|
|
221
231
|
register_prompts(mcp)
|
|
222
232
|
|
|
223
233
|
# Register schema resources
|
|
224
|
-
from .resources import (
|
|
225
|
-
register_agent_resources,
|
|
226
|
-
register_file_resources,
|
|
227
|
-
register_schema_resources,
|
|
228
|
-
register_status_resources,
|
|
229
|
-
)
|
|
230
|
-
|
|
231
234
|
register_schema_resources(mcp)
|
|
232
235
|
register_agent_resources(mcp)
|
|
233
236
|
register_file_resources(mcp)
|
rem/api/mcp_router/tools.py
CHANGED
|
@@ -1132,3 +1132,82 @@ async def save_agent(
|
|
|
1132
1132
|
result["message"] = f"Agent '{name}' saved. Use `/custom-agent {name}` to chat with it."
|
|
1133
1133
|
|
|
1134
1134
|
return result
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
# =============================================================================
|
|
1138
|
+
# Test/Debug Tools (for development only)
|
|
1139
|
+
# =============================================================================
|
|
1140
|
+
|
|
1141
|
+
@mcp_tool_error_handler
|
|
1142
|
+
async def test_error_handling(
|
|
1143
|
+
error_type: Literal["exception", "error_response", "timeout", "success"] = "success",
|
|
1144
|
+
delay_seconds: float = 0,
|
|
1145
|
+
error_message: str = "Test error occurred",
|
|
1146
|
+
) -> dict[str, Any]:
|
|
1147
|
+
"""
|
|
1148
|
+
Test tool for simulating different error scenarios.
|
|
1149
|
+
|
|
1150
|
+
**FOR DEVELOPMENT/TESTING ONLY** - This tool helps verify that error
|
|
1151
|
+
handling works correctly through the streaming layer.
|
|
1152
|
+
|
|
1153
|
+
Args:
|
|
1154
|
+
error_type: Type of error to simulate:
|
|
1155
|
+
- "success": Returns successful response (default)
|
|
1156
|
+
- "exception": Raises an exception (tests @mcp_tool_error_handler)
|
|
1157
|
+
- "error_response": Returns {"status": "error", ...} dict
|
|
1158
|
+
- "timeout": Delays for 60 seconds (simulates timeout)
|
|
1159
|
+
delay_seconds: Optional delay before responding (0-10 seconds)
|
|
1160
|
+
error_message: Custom error message for error scenarios
|
|
1161
|
+
|
|
1162
|
+
Returns:
|
|
1163
|
+
Dict with test results or error information
|
|
1164
|
+
|
|
1165
|
+
Examples:
|
|
1166
|
+
# Test successful response
|
|
1167
|
+
test_error_handling(error_type="success")
|
|
1168
|
+
|
|
1169
|
+
# Test exception handling
|
|
1170
|
+
test_error_handling(error_type="exception", error_message="Database connection failed")
|
|
1171
|
+
|
|
1172
|
+
# Test error response format
|
|
1173
|
+
test_error_handling(error_type="error_response", error_message="Resource not found")
|
|
1174
|
+
|
|
1175
|
+
# Test with delay
|
|
1176
|
+
test_error_handling(error_type="success", delay_seconds=2)
|
|
1177
|
+
"""
|
|
1178
|
+
import asyncio
|
|
1179
|
+
|
|
1180
|
+
logger.info(f"test_error_handling called: type={error_type}, delay={delay_seconds}")
|
|
1181
|
+
|
|
1182
|
+
# Apply delay (capped at 10 seconds for safety)
|
|
1183
|
+
if delay_seconds > 0:
|
|
1184
|
+
await asyncio.sleep(min(delay_seconds, 10))
|
|
1185
|
+
|
|
1186
|
+
if error_type == "exception":
|
|
1187
|
+
# This tests the @mcp_tool_error_handler decorator
|
|
1188
|
+
raise RuntimeError(f"TEST EXCEPTION: {error_message}")
|
|
1189
|
+
|
|
1190
|
+
elif error_type == "error_response":
|
|
1191
|
+
# This tests how the streaming layer handles error status responses
|
|
1192
|
+
return {
|
|
1193
|
+
"status": "error",
|
|
1194
|
+
"error": error_message,
|
|
1195
|
+
"error_code": "TEST_ERROR",
|
|
1196
|
+
"recoverable": True,
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
elif error_type == "timeout":
|
|
1200
|
+
# Simulate a very long operation (for testing client-side timeouts)
|
|
1201
|
+
await asyncio.sleep(60)
|
|
1202
|
+
return {"status": "success", "message": "Timeout test completed (should not reach here)"}
|
|
1203
|
+
|
|
1204
|
+
else: # success
|
|
1205
|
+
return {
|
|
1206
|
+
"status": "success",
|
|
1207
|
+
"message": "Test completed successfully",
|
|
1208
|
+
"test_data": {
|
|
1209
|
+
"error_type": error_type,
|
|
1210
|
+
"delay_applied": delay_seconds,
|
|
1211
|
+
"timestamp": str(asyncio.get_event_loop().time()),
|
|
1212
|
+
},
|
|
1213
|
+
}
|
rem/api/routers/auth.py
CHANGED
|
@@ -30,14 +30,17 @@ Access Control Flow (send-code):
|
|
|
30
30
|
│ ├── Yes → Check user.tier
|
|
31
31
|
│ │ ├── tier == BLOCKED → Reject "Account is blocked"
|
|
32
32
|
│ │ └── tier != BLOCKED → Allow (send code, existing users grandfathered)
|
|
33
|
-
│ └── No (new user) → Check
|
|
34
|
-
│ ├──
|
|
35
|
-
│
|
|
36
|
-
│
|
|
37
|
-
│
|
|
33
|
+
│ └── No (new user) → Check subscriber list first
|
|
34
|
+
│ ├── Email in subscribers table? → Allow (create user & send code)
|
|
35
|
+
│ └── Not a subscriber → Check EMAIL__TRUSTED_EMAIL_DOMAINS
|
|
36
|
+
│ ├── Setting configured → domain in trusted list?
|
|
37
|
+
│ │ ├── Yes → Create user & send code
|
|
38
|
+
│ │ └── No → Reject "Email domain not allowed for signup"
|
|
39
|
+
│ └── Not configured (empty) → Create user & send code (no restrictions)
|
|
38
40
|
|
|
39
41
|
Key Behaviors:
|
|
40
42
|
- Existing users: Always allowed to login (unless tier=BLOCKED)
|
|
43
|
+
- Subscribers: Always allowed to login (regardless of email domain)
|
|
41
44
|
- New users: Must have email from trusted domain (if EMAIL__TRUSTED_EMAIL_DOMAINS is set)
|
|
42
45
|
- No restrictions: Leave EMAIL__TRUSTED_EMAIL_DOMAINS empty to allow all domains
|
|
43
46
|
|
rem/cli/commands/ask.py
CHANGED
|
@@ -75,7 +75,7 @@ async def run_agent_streaming(
|
|
|
75
75
|
"""
|
|
76
76
|
Run agent in streaming mode using agent.iter() with usage limits.
|
|
77
77
|
|
|
78
|
-
Design Pattern
|
|
78
|
+
Design Pattern:
|
|
79
79
|
- Use agent.iter() for complete execution with tool call visibility
|
|
80
80
|
- run_stream() stops after first output, missing tool calls
|
|
81
81
|
- Stream tool call markers: [Calling: tool_name]
|