From 1a175e604b7512c125af04ebf68302fba1213261 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 30 Jun 2026 06:24:37 +0800 Subject: [PATCH] {"schema":"decodex/commit/1","summary":"Split MCP server core tool router into tools module after strict validation.","authority":"manual"} --- .../dreaming_reports.rs | 87 ++--- .../trace_replay_reports.rs | 11 +- apps/elf-mcp/src/app/server.rs | 331 +----------------- apps/elf-mcp/src/app/server/tools.rs | 1 + apps/elf-mcp/src/app/server/tools/core.rs | 331 ++++++++++++++++++ 5 files changed, 392 insertions(+), 369 deletions(-) create mode 100644 apps/elf-mcp/src/app/server/tools/core.rs diff --git a/apps/elf-eval/tests/real_world_job_benchmark/dreaming_reports.rs b/apps/elf-eval/tests/real_world_job_benchmark/dreaming_reports.rs index f16482d4..a4bce786 100644 --- a/apps/elf-eval/tests/real_world_job_benchmark/dreaming_reports.rs +++ b/apps/elf-eval/tests/real_world_job_benchmark/dreaming_reports.rs @@ -1,34 +1,45 @@ use std::{fs, path::Path}; -use color_eyre::{self, eyre}; +use color_eyre::{self, Result, eyre}; use serde_json::Value; use crate::support; -fn read_rust_module_sources(src_dir: &Path, module_name: &str) -> color_eyre::Result { +fn read_rust_module_sources(src_dir: &Path, module_name: &str) -> Result { let module_root = src_dir.join(format!("{module_name}.rs")); let module_dir = src_dir.join(module_name); let mut source = fs::read_to_string(module_root)?; if module_dir.is_dir() { - let mut entries = fs::read_dir(module_dir)? - .map(|entry| entry.map(|entry| entry.path())) - .collect::>>()?; + append_rust_sources(module_dir.as_path(), &mut source)?; + } - entries.retain(|path| path.extension().is_some_and(|extension| extension == "rs")); - entries.sort(); + Ok(source) +} - for path in entries { +fn append_rust_sources(dir: &Path, source: &mut String) -> Result<()> { + let mut entries = Vec::new(); + + for entry in fs::read_dir(dir)? { + entries.push(entry?.path()); + } + + entries.sort(); + + for path in entries { + if path.is_dir() { + append_rust_sources(path.as_path(), source)?; + } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") { source.push('\n'); - source.push_str(&fs::read_to_string(path)?); + source.push_str(fs::read_to_string(path)?.as_str()); } } - Ok(source) + Ok(()) } #[test] -fn live_temporal_reconciliation_report_records_xy905_before_after() -> color_eyre::Result<()> { +fn live_temporal_reconciliation_report_records_xy905_before_after() -> Result<()> { let report = serde_json::from_str::(&fs::read_to_string( support::live_temporal_reconciliation_report_json_path()?, )?)?; @@ -117,8 +128,7 @@ fn live_temporal_reconciliation_report_records_xy905_before_after() -> color_eyr } #[test] -fn dreaming_competitor_strength_retest_report_closes_xy955_without_overclaims() --> color_eyre::Result<()> { +fn dreaming_competitor_strength_retest_report_closes_xy955_without_overclaims() -> Result<()> { let report = serde_json::from_str::(&fs::read_to_string( support::dreaming_competitor_strength_retest_report_json_path()?, )?)?; @@ -177,7 +187,7 @@ fn dreaming_competitor_strength_retest_report_closes_xy955_without_overclaims() } #[test] -fn qmd_debug_ergonomics_dreaming_retest_report_preserves_qmd_edge() -> color_eyre::Result<()> { +fn qmd_debug_ergonomics_dreaming_retest_report_preserves_qmd_edge() -> Result<()> { let report = serde_json::from_str::(&fs::read_to_string( support::qmd_debug_ergonomics_dreaming_retest_report_json_path()?, )?)?; @@ -195,7 +205,7 @@ fn qmd_debug_ergonomics_dreaming_retest_report_preserves_qmd_edge() -> color_eyr Ok(()) } -fn assert_qmd_debug_retest_summary(report: &Value) -> color_eyre::Result<()> { +fn assert_qmd_debug_retest_summary(report: &Value) -> Result<()> { assert_eq!( report.pointer("/schema").and_then(Value::as_str), Some("elf.qmd_debug_ergonomics_dreaming_retest_report/v1") @@ -231,7 +241,7 @@ fn assert_qmd_debug_retest_summary(report: &Value) -> color_eyre::Result<()> { Ok(()) } -fn assert_qmd_debug_retest_command_and_adapters(report: &Value) -> color_eyre::Result<()> { +fn assert_qmd_debug_retest_command_and_adapters(report: &Value) -> Result<()> { let command = support::find_by_field( support::array_at(report, "/commands")?, "/command", @@ -263,7 +273,7 @@ fn assert_qmd_debug_retest_command_and_adapters(report: &Value) -> color_eyre::R Ok(()) } -fn assert_qmd_debug_retest_scenarios(report: &Value) -> color_eyre::Result<()> { +fn assert_qmd_debug_retest_scenarios(report: &Value) -> Result<()> { let scenarios = support::array_at(report, "/scenario_retests")?; let top10 = support::find_by_field(scenarios, "/scenario_id", "qmd_default_top10_candidate_artifact")?; @@ -310,7 +320,7 @@ fn assert_qmd_debug_retest_scenarios(report: &Value) -> color_eyre::Result<()> { Ok(()) } -fn assert_qmd_debug_retest_boundaries(report: &Value) -> color_eyre::Result<()> { +fn assert_qmd_debug_retest_boundaries(report: &Value) -> Result<()> { assert!(support::array_contains_str( report, "/claim_boundaries/allowed", @@ -351,8 +361,7 @@ fn assert_qmd_debug_retest_markdown_and_indexes( } #[test] -fn openviking_trajectory_materialization_report_preserves_blocked_gates() -> color_eyre::Result<()> -{ +fn openviking_trajectory_materialization_report_preserves_blocked_gates() -> Result<()> { let report = serde_json::from_str::(&fs::read_to_string( support::openviking_trajectory_materialization_report_json_path()?, )?)?; @@ -375,7 +384,7 @@ fn openviking_trajectory_materialization_report_preserves_blocked_gates() -> col } #[test] -fn letta_core_archive_export_readback_report_preserves_blocked_gates() -> color_eyre::Result<()> { +fn letta_core_archive_export_readback_report_preserves_blocked_gates() -> Result<()> { let report = serde_json::from_str::(&fs::read_to_string( support::letta_core_archive_export_readback_report_json_path()?, )?)?; @@ -474,7 +483,7 @@ fn letta_core_archive_export_readback_report_preserves_blocked_gates() -> color_ } #[test] -fn service_native_dreaming_readback_report_materializes_public_jobs() -> color_eyre::Result<()> { +fn service_native_dreaming_readback_report_materializes_public_jobs() -> Result<()> { let report = serde_json::from_str::(&fs::read_to_string( support::service_native_dreaming_readback_report_json_path()?, )?)?; @@ -494,7 +503,7 @@ fn service_native_dreaming_readback_report_materializes_public_jobs() -> color_e Ok(()) } -fn assert_service_native_dreaming_report_summary(report: &Value) -> color_eyre::Result<()> { +fn assert_service_native_dreaming_report_summary(report: &Value) -> Result<()> { assert_eq!( report.pointer("/adapter/adapter_id").and_then(Value::as_str), Some("elf_service_native_dreaming") @@ -540,7 +549,7 @@ fn assert_service_native_dreaming_report_summary(report: &Value) -> color_eyre:: Ok(()) } -fn assert_service_native_dreaming_report_jobs(report: &Value) -> color_eyre::Result<()> { +fn assert_service_native_dreaming_report_jobs(report: &Value) -> Result<()> { let jobs = support::array_at(report, "/jobs")?; let memory = support::find_by_field(jobs, "/job_id", "memory-summary-source-trace-001")?; let daily = support::find_by_field(jobs, "/job_id", "proactive-daily-project-brief-001")?; @@ -571,9 +580,7 @@ fn assert_service_native_dreaming_report_jobs(report: &Value) -> color_eyre::Res Ok(()) } -fn assert_service_native_dreaming_materialization( - materialization: &Value, -) -> color_eyre::Result<()> { +fn assert_service_native_dreaming_materialization(materialization: &Value) -> Result<()> { assert_eq!( materialization.pointer("/schema").and_then(Value::as_str), Some("elf.real_world_live_adapter_materialization/v1") @@ -650,7 +657,7 @@ fn assert_service_native_dreaming_docs(markdown: &str, benchmarking_index: &str, } #[test] -fn dreaming_review_queue_report_wires_reviewable_policy_contract() -> color_eyre::Result<()> { +fn dreaming_review_queue_report_wires_reviewable_policy_contract() -> Result<()> { let report = serde_json::from_str::(&fs::read_to_string( support::dreaming_review_queue_report_json_path()?, )?)?; @@ -664,7 +671,7 @@ fn dreaming_review_queue_report_wires_reviewable_policy_contract() -> color_eyre )?; let service_lib = fs::read_to_string(workspace.join("packages/elf-service/src/lib.rs"))?; let routes = read_rust_module_sources(&workspace.join("apps/elf-api/src"), "routes")?; - let mcp = fs::read_to_string(workspace.join("apps/elf-mcp/src/app/server.rs"))?; + let mcp = read_rust_module_sources(&workspace.join("apps/elf-mcp/src/app"), "server")?; let consolidation_spec = fs::read_to_string(workspace.join("docs/spec/system_consolidation_proposals_v1.md"))?; let service_spec = @@ -742,7 +749,7 @@ fn dreaming_review_queue_report_wires_reviewable_policy_contract() -> color_eyre Ok(()) } -fn assert_openviking_trajectory_materialization_summary(report: &Value) -> color_eyre::Result<()> { +fn assert_openviking_trajectory_materialization_summary(report: &Value) -> Result<()> { assert_eq!( report.pointer("/schema").and_then(Value::as_str), Some("elf.openviking_trajectory_materialization_report/v1") @@ -774,7 +781,7 @@ fn assert_openviking_trajectory_materialization_summary(report: &Value) -> color Ok(()) } -fn assert_openviking_trajectory_materialization_command(report: &Value) -> color_eyre::Result<()> { +fn assert_openviking_trajectory_materialization_command(report: &Value) -> Result<()> { let command = support::find_by_field( support::array_at(report, "/commands")?, "/command", @@ -799,9 +806,7 @@ fn assert_openviking_trajectory_materialization_command(report: &Value) -> color Ok(()) } -fn assert_openviking_trajectory_materialization_scenarios( - report: &Value, -) -> color_eyre::Result<()> { +fn assert_openviking_trajectory_materialization_scenarios(report: &Value) -> Result<()> { let scenarios = support::array_at(report, "/scenario_materialization")?; let staged = support::find_by_field( scenarios, @@ -857,9 +862,7 @@ fn assert_openviking_trajectory_materialization_scenarios( Ok(()) } -fn assert_openviking_trajectory_materialization_boundaries( - report: &Value, -) -> color_eyre::Result<()> { +fn assert_openviking_trajectory_materialization_boundaries(report: &Value) -> Result<()> { assert_eq!( report.pointer("/improvement_regression_readback/improved").and_then(Value::as_u64), Some(0) @@ -910,7 +913,7 @@ fn assert_openviking_trajectory_materialization_markdown_and_indexes( assert!(readme.contains("3 typed blockers with 9/9 evidence coverage")); } -fn assert_xy955_commands(report: &Value) -> color_eyre::Result<()> { +fn assert_xy955_commands(report: &Value) -> Result<()> { let commands = support::array_at(report, "/commands")?; let aggregate = support::find_by_field(commands, "/command", "cargo make real-world-memory")?; let graph_rag = @@ -950,7 +953,7 @@ fn assert_xy955_commands(report: &Value) -> color_eyre::Result<()> { Ok(()) } -fn assert_xy955_stage_closeout(report: &Value) -> color_eyre::Result<()> { +fn assert_xy955_stage_closeout(report: &Value) -> Result<()> { let stages = support::array_at(report, "/stage_closeout")?; assert_eq!(stages.len(), 8); @@ -992,7 +995,7 @@ fn assert_xy955_stage_closeout(report: &Value) -> color_eyre::Result<()> { Ok(()) } -fn assert_xy955_scenario_retests(report: &Value) -> color_eyre::Result<()> { +fn assert_xy955_scenario_retests(report: &Value) -> Result<()> { let scenarios = support::array_at(report, "/scenario_retests")?; let qmd = support::find_by_field(scenarios, "/scenario_id", "qmd_debug_ergonomics")?; let mem0 = support::find_by_field( @@ -1036,7 +1039,7 @@ fn assert_xy955_scenario_retests(report: &Value) -> color_eyre::Result<()> { Ok(()) } -fn assert_xy955_optimization_queue(report: &Value) -> color_eyre::Result<()> { +fn assert_xy955_optimization_queue(report: &Value) -> Result<()> { let queue = support::array_at(report, "/optimization_queue")?; let qmd = support::find_by_field(queue, "/issue", "XY-923")?; let private_provider = support::find_by_field(queue, "/issue", "XY-930")?; @@ -1059,7 +1062,7 @@ fn assert_xy955_optimization_queue(report: &Value) -> color_eyre::Result<()> { Ok(()) } -fn assert_xy955_follow_up_issue_briefs(report: &Value) -> color_eyre::Result<()> { +fn assert_xy955_follow_up_issue_briefs(report: &Value) -> Result<()> { let existing = support::array_at(report, "/follow_up_issue_briefs/existing")?; let proposed = support::array_at(report, "/follow_up_issue_briefs/proposed")?; let qmd = support::find_by_field(existing, "/issue", "XY-923")?; diff --git a/apps/elf-eval/tests/real_world_job_benchmark/trace_replay_reports.rs b/apps/elf-eval/tests/real_world_job_benchmark/trace_replay_reports.rs index c3ace188..a5fcba97 100644 --- a/apps/elf-eval/tests/real_world_job_benchmark/trace_replay_reports.rs +++ b/apps/elf-eval/tests/real_world_job_benchmark/trace_replay_reports.rs @@ -17,6 +17,14 @@ fn graph_report_service_sources(workspace: &Path) -> Result { Ok(source) } +fn mcp_server_sources(workspace: &Path) -> Result { + let mut source = fs::read_to_string(workspace.join("apps/elf-mcp/src/app/server.rs"))?; + + append_rust_sources(workspace.join("apps/elf-mcp/src/app/server").as_path(), &mut source)?; + + Ok(source) +} + fn append_rust_sources(dir: &Path, source: &mut String) -> Result<()> { let mut entries = Vec::new(); @@ -47,8 +55,7 @@ fn graph_topic_map_report_wires_source_backed_graph_lite_readback() -> Result<() let graph_report_service = graph_report_service_sources(&workspace)?; let api_routes = fs::read_to_string(support::workspace_root()?.join("apps/elf-api/src/routes.rs"))?; - let mcp_server = - fs::read_to_string(support::workspace_root()?.join("apps/elf-mcp/src/app/server.rs"))?; + let mcp_server = mcp_server_sources(&workspace)?; let graph_spec = fs::read_to_string( support::workspace_root()?.join("docs/spec/system_graph_memory_postgres_v1.md"), )?; diff --git a/apps/elf-mcp/src/app/server.rs b/apps/elf-mcp/src/app/server.rs index 1d060ded..33910068 100644 --- a/apps/elf-mcp/src/app/server.rs +++ b/apps/elf-mcp/src/app/server.rs @@ -6,23 +6,15 @@ mod tools; pub use runtime::serve_mcp; -use color_eyre::Result; -use rmcp::{ - ErrorData, - handler::server::router::tool::ToolRouter, - model::{CallToolResult, JsonObject}, -}; +use rmcp::handler::server::router::tool::ToolRouter; +#[cfg(test)] use schemas::{ - core_blocks_get_schema, dreaming_review_queue_schema, entity_memory_get_schema, - events_ingest_schema, graph_query_schema, graph_report_schema, notes_get_schema, - notes_ingest_schema, notes_list_schema, notes_patch_schema, notes_publish_schema, - notes_unpublish_schema, recall_debug_panel_schema, searches_create_schema, searches_get_schema, - searches_notes_schema, searches_timeline_schema, space_grant_revoke_schema, - space_grant_upsert_schema, space_grants_list_schema, work_journal_entry_create_schema, - work_journal_entry_get_schema, work_journal_session_readback_schema, + docs_excerpts_get_schema, docs_put_schema, docs_search_l0_schema, notes_ingest_schema, + recall_debug_panel_schema, searches_create_schema, searches_get_schema, searches_notes_schema, + searches_timeline_schema, work_journal_entry_create_schema, + work_journal_session_readback_schema, }; -#[cfg(test)] use schemas::{docs_excerpts_get_schema, docs_put_schema, docs_search_l0_schema}; use state::{ElfContextHeaders, ElfMcp, HttpMethod}; #[cfg(test)] use support::is_authorized; use support::{ @@ -36,317 +28,6 @@ const HEADER_READ_PROFILE: &str = "X-ELF-Read-Profile"; const HEADER_REQUEST_ID: &str = "X-ELF-Request-Id"; const HEADER_AUTHORIZATION: &str = "Authorization"; -#[rmcp::tool_router(router = core_tool_router, vis = "pub(in crate::app::server)")] -impl ElfMcp { - #[rmcp::tool( - name = "elf_notes_ingest", - description = "Ingest deterministic notes into ELF. This tool never calls an LLM.", - input_schema = notes_ingest_schema() - )] - async fn elf_notes_ingest(&self, params: JsonObject) -> Result { - self.forward(HttpMethod::Post, "/v2/notes/ingest", params, None).await - } - - #[rmcp::tool( - name = "elf_graph_query", - description = "Query graph entities and relations by structured criteria.", - input_schema = graph_query_schema() - )] - async fn elf_graph_query(&self, params: JsonObject) -> Result { - self.forward(HttpMethod::Post, "/v2/graph/query", params, None).await - } - - #[rmcp::tool( - name = "elf_graph_report", - description = "Build a source-backed graph topic map with current, historical, future, inferred, ambiguous, stale, and superseded fact markers.", - input_schema = graph_report_schema() - )] - async fn elf_graph_report(&self, params: JsonObject) -> Result { - self.forward(HttpMethod::Post, "/v2/graph/report", params, None).await - } - - #[rmcp::tool( - name = "elf_events_ingest", - description = "Ingest an event by extracting evidence-bound notes using the configured LLM extractor.", - input_schema = events_ingest_schema() - )] - async fn elf_events_ingest(&self, params: JsonObject) -> Result { - self.forward(HttpMethod::Post, "/v2/events/ingest", params, None).await - } - - #[rmcp::tool( - name = "elf_work_journal_entry_create", - description = "Capture one source-adjacent Work Journal entry with source refs, redaction, next-step, rejected-option, and promotion-boundary metadata. Journal content is not authoritative memory.", - input_schema = work_journal_entry_create_schema() - )] - async fn elf_work_journal_entry_create( - &self, - params: JsonObject, - ) -> Result { - self.forward(HttpMethod::Post, "/v2/work-journal/entries", params, None).await - } - - #[rmcp::tool( - name = "elf_work_journal_entry_get", - description = "Fetch one readable Work Journal entry by entry_id.", - input_schema = work_journal_entry_get_schema() - )] - async fn elf_work_journal_entry_get( - &self, - mut params: JsonObject, - ) -> Result { - let entry_id = support::take_required_string(&mut params, "entry_id")?; - let path = format!("/v2/work-journal/entries/{entry_id}"); - - self.forward(HttpMethod::Get, &path, JsonObject::new(), None).await - } - - #[rmcp::tool( - name = "elf_work_journal_session_readback", - description = "Read newest Work Journal entries for a session and return a where_stopped projection with journal evidence. Current-fact answers still require accepted memory or knowledge authority.", - input_schema = work_journal_session_readback_schema() - )] - async fn elf_work_journal_session_readback( - &self, - mut params: JsonObject, - ) -> Result { - // read_profile is part of the MCP server configuration and is not client-controlled. - let _ = support::take_optional_string(&mut params, "read_profile")?; - - self.forward(HttpMethod::Post, "/v2/work-journal/readback", params, None).await - } - - #[rmcp::tool( - name = "elf_core_blocks_get", - description = "Fetch core memory blocks explicitly attached to the configured agent and read profile. This is separate from archival search.", - input_schema = core_blocks_get_schema() - )] - async fn elf_core_blocks_get( - &self, - mut params: JsonObject, - ) -> Result { - // read_profile is part of the MCP server configuration and is not client-controlled. - let _ = support::take_optional_string(&mut params, "read_profile")?; - - self.forward(HttpMethod::Get, "/v2/core-blocks", params, None).await - } - - #[rmcp::tool( - name = "elf_entity_memory_get", - description = "Fetch an entity-scoped memory view across attached core blocks and graph-linked archival notes.", - input_schema = entity_memory_get_schema() - )] - async fn elf_entity_memory_get( - &self, - mut params: JsonObject, - ) -> Result { - // read_profile is part of the MCP server configuration and is not client-controlled. - let _ = support::take_optional_string(&mut params, "read_profile")?; - - self.forward(HttpMethod::Get, "/v2/entity-memory", params, None).await - } - - #[rmcp::tool( - name = "elf_dreaming_review_queue", - description = "List source-backed Dreaming review queue proposals with variants, affected refs, lint flags, policy gates, and review audit.", - input_schema = dreaming_review_queue_schema() - )] - async fn elf_dreaming_review_queue( - &self, - params: JsonObject, - ) -> Result { - self.forward(HttpMethod::Get, "/v2/admin/dreaming/review-queue", params, None).await - } - - #[rmcp::tool( - name = "elf_recall_debug_panel", - description = "Build an agent-facing cross-layer recall/debug panel and deterministic recall_trace over memory traces, source documents, knowledge pages, graph facts, and Dreaming proposals.", - input_schema = recall_debug_panel_schema() - )] - async fn elf_recall_debug_panel( - &self, - params: JsonObject, - ) -> Result { - support::reject_context_override_params(¶ms)?; - - self.forward(HttpMethod::Post, "/v2/recall-debug/panel", params, None).await - } - - #[rmcp::tool( - name = "elf_searches_create", - description = "Create a search session using quick-find or planned-search mode. Response includes optional trajectory_summary for staged retrieval progress.", - input_schema = searches_create_schema() - )] - async fn elf_searches_create( - &self, - mut params: JsonObject, - ) -> Result { - // read_profile is part of the MCP server configuration and is not client-controlled. - let _ = support::take_optional_string(&mut params, "read_profile")?; - - self.forward(HttpMethod::Post, "/v2/searches", params, None).await - } - - #[rmcp::tool( - name = "elf_searches_get", - description = "Fetch a search session index view by search_id, including optional trajectory_summary.", - input_schema = searches_get_schema() - )] - async fn elf_searches_get(&self, mut params: JsonObject) -> Result { - let search_id = support::take_required_string(&mut params, "search_id")?; - let path = format!("/v2/searches/{search_id}"); - - self.forward(HttpMethod::Get, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_searches_timeline", - description = "Build a timeline view from a search session.", - input_schema = searches_timeline_schema() - )] - async fn elf_searches_timeline( - &self, - mut params: JsonObject, - ) -> Result { - let search_id = support::take_required_string(&mut params, "search_id")?; - let path = format!("/v2/searches/{search_id}/timeline"); - - self.forward(HttpMethod::Get, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_searches_notes", - description = "Fetch note details for selected note_ids from a search session. l0/l1 strip evidence/source_ref; l2 returns full detail.", - input_schema = searches_notes_schema() - )] - async fn elf_searches_notes( - &self, - mut params: JsonObject, - ) -> Result { - let search_id = support::take_required_string(&mut params, "search_id")?; - let path = format!("/v2/searches/{search_id}/notes"); - - self.forward(HttpMethod::Post, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_notes_list", - description = "List notes in a tenant and project with optional filters.", - input_schema = notes_list_schema() - )] - async fn elf_notes_list(&self, params: JsonObject) -> Result { - self.forward(HttpMethod::Get, "/v2/notes", params, None).await - } - - #[rmcp::tool( - name = "elf_notes_get", - description = "Fetch a single note by note_id.", - input_schema = notes_get_schema() - )] - async fn elf_notes_get(&self, mut params: JsonObject) -> Result { - let note_id = support::take_required_string(&mut params, "note_id")?; - let path = format!("/v2/notes/{note_id}"); - - self.forward(HttpMethod::Get, &path, JsonObject::new(), None).await - } - - #[rmcp::tool( - name = "elf_notes_patch", - description = "Patch a note by note_id. Only provided fields are updated.", - input_schema = notes_patch_schema() - )] - async fn elf_notes_patch(&self, mut params: JsonObject) -> Result { - let note_id = support::take_required_string(&mut params, "note_id")?; - let path = format!("/v2/notes/{note_id}"); - - self.forward(HttpMethod::Patch, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_notes_delete", - description = "Delete a note by note_id.", - input_schema = notes_get_schema() - )] - async fn elf_notes_delete(&self, mut params: JsonObject) -> Result { - let note_id = support::take_required_string(&mut params, "note_id")?; - let path = format!("/v2/notes/{note_id}"); - - self.forward(HttpMethod::Delete, &path, JsonObject::new(), None).await - } - - #[rmcp::tool( - name = "elf_notes_publish", - description = "Publish a note from agent_private into a shared space (team_shared or org_shared).", - input_schema = notes_publish_schema() - )] - async fn elf_notes_publish(&self, mut params: JsonObject) -> Result { - let note_id = support::take_required_string(&mut params, "note_id")?; - let path = format!("/v2/notes/{note_id}/publish"); - - self.forward(HttpMethod::Post, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_notes_unpublish", - description = "Unpublish a shared note back into agent_private scope.", - input_schema = notes_unpublish_schema() - )] - async fn elf_notes_unpublish( - &self, - mut params: JsonObject, - ) -> Result { - let note_id = support::take_required_string(&mut params, "note_id")?; - let path = format!("/v2/notes/{note_id}/unpublish"); - - self.forward(HttpMethod::Post, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_space_grants_list", - description = "List sharing grants for a space (team_shared or org_shared).", - input_schema = space_grants_list_schema() - )] - async fn elf_space_grants_list( - &self, - mut params: JsonObject, - ) -> Result { - let space = support::take_required_string(&mut params, "space")?; - let path = format!("/v2/spaces/{space}/grants"); - - self.forward(HttpMethod::Get, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_space_grant_upsert", - description = "Upsert a sharing grant for a space (team_shared or org_shared).", - input_schema = space_grant_upsert_schema() - )] - async fn elf_space_grant_upsert( - &self, - mut params: JsonObject, - ) -> Result { - let space = support::take_required_string(&mut params, "space")?; - let path = format!("/v2/spaces/{space}/grants"); - - self.forward(HttpMethod::Post, &path, params, None).await - } - - #[rmcp::tool( - name = "elf_space_grant_revoke", - description = "Revoke a sharing grant for a space (team_shared or org_shared).", - input_schema = space_grant_revoke_schema() - )] - async fn elf_space_grant_revoke( - &self, - mut params: JsonObject, - ) -> Result { - let space = support::take_required_string(&mut params, "space")?; - let path = format!("/v2/spaces/{space}/grants/revoke"); - - self.forward(HttpMethod::Post, &path, params, None).await - } -} - impl ElfMcp { pub(in crate::app::server) fn tool_router() -> ToolRouter { Self::core_tool_router() + Self::docs_tool_router() + Self::admin_tool_router() diff --git a/apps/elf-mcp/src/app/server/tools.rs b/apps/elf-mcp/src/app/server/tools.rs index 7c61555f..ffca1d37 100644 --- a/apps/elf-mcp/src/app/server/tools.rs +++ b/apps/elf-mcp/src/app/server/tools.rs @@ -1,2 +1,3 @@ mod admin; +mod core; mod docs; diff --git a/apps/elf-mcp/src/app/server/tools/core.rs b/apps/elf-mcp/src/app/server/tools/core.rs new file mode 100644 index 00000000..e848c476 --- /dev/null +++ b/apps/elf-mcp/src/app/server/tools/core.rs @@ -0,0 +1,331 @@ +use color_eyre::Result; +use rmcp::{ + ErrorData, + model::{CallToolResult, JsonObject}, +}; + +use crate::app::server::{ + ElfMcp, HttpMethod, + schemas::{ + core_blocks_get_schema, dreaming_review_queue_schema, entity_memory_get_schema, + events_ingest_schema, graph_query_schema, graph_report_schema, notes_get_schema, + notes_ingest_schema, notes_list_schema, notes_patch_schema, notes_publish_schema, + notes_unpublish_schema, recall_debug_panel_schema, searches_create_schema, + searches_get_schema, searches_notes_schema, searches_timeline_schema, + space_grant_revoke_schema, space_grant_upsert_schema, space_grants_list_schema, + work_journal_entry_create_schema, work_journal_entry_get_schema, + work_journal_session_readback_schema, + }, + support, +}; + +#[rmcp::tool_router(router = core_tool_router, vis = "pub(in crate::app::server)")] +impl ElfMcp { + #[rmcp::tool( + name = "elf_notes_ingest", + description = "Ingest deterministic notes into ELF. This tool never calls an LLM.", + input_schema = notes_ingest_schema() + )] + async fn elf_notes_ingest(&self, params: JsonObject) -> Result { + self.forward(HttpMethod::Post, "/v2/notes/ingest", params, None).await + } + + #[rmcp::tool( + name = "elf_graph_query", + description = "Query graph entities and relations by structured criteria.", + input_schema = graph_query_schema() + )] + async fn elf_graph_query(&self, params: JsonObject) -> Result { + self.forward(HttpMethod::Post, "/v2/graph/query", params, None).await + } + + #[rmcp::tool( + name = "elf_graph_report", + description = "Build a source-backed graph topic map with current, historical, future, inferred, ambiguous, stale, and superseded fact markers.", + input_schema = graph_report_schema() + )] + async fn elf_graph_report(&self, params: JsonObject) -> Result { + self.forward(HttpMethod::Post, "/v2/graph/report", params, None).await + } + + #[rmcp::tool( + name = "elf_events_ingest", + description = "Ingest an event by extracting evidence-bound notes using the configured LLM extractor.", + input_schema = events_ingest_schema() + )] + async fn elf_events_ingest(&self, params: JsonObject) -> Result { + self.forward(HttpMethod::Post, "/v2/events/ingest", params, None).await + } + + #[rmcp::tool( + name = "elf_work_journal_entry_create", + description = "Capture one source-adjacent Work Journal entry with source refs, redaction, next-step, rejected-option, and promotion-boundary metadata. Journal content is not authoritative memory.", + input_schema = work_journal_entry_create_schema() + )] + async fn elf_work_journal_entry_create( + &self, + params: JsonObject, + ) -> Result { + self.forward(HttpMethod::Post, "/v2/work-journal/entries", params, None).await + } + + #[rmcp::tool( + name = "elf_work_journal_entry_get", + description = "Fetch one readable Work Journal entry by entry_id.", + input_schema = work_journal_entry_get_schema() + )] + async fn elf_work_journal_entry_get( + &self, + mut params: JsonObject, + ) -> Result { + let entry_id = support::take_required_string(&mut params, "entry_id")?; + let path = format!("/v2/work-journal/entries/{entry_id}"); + + self.forward(HttpMethod::Get, &path, JsonObject::new(), None).await + } + + #[rmcp::tool( + name = "elf_work_journal_session_readback", + description = "Read newest Work Journal entries for a session and return a where_stopped projection with journal evidence. Current-fact answers still require accepted memory or knowledge authority.", + input_schema = work_journal_session_readback_schema() + )] + async fn elf_work_journal_session_readback( + &self, + mut params: JsonObject, + ) -> Result { + // read_profile is part of the MCP server configuration and is not client-controlled. + let _ = support::take_optional_string(&mut params, "read_profile")?; + + self.forward(HttpMethod::Post, "/v2/work-journal/readback", params, None).await + } + + #[rmcp::tool( + name = "elf_core_blocks_get", + description = "Fetch core memory blocks explicitly attached to the configured agent and read profile. This is separate from archival search.", + input_schema = core_blocks_get_schema() + )] + async fn elf_core_blocks_get( + &self, + mut params: JsonObject, + ) -> Result { + // read_profile is part of the MCP server configuration and is not client-controlled. + let _ = support::take_optional_string(&mut params, "read_profile")?; + + self.forward(HttpMethod::Get, "/v2/core-blocks", params, None).await + } + + #[rmcp::tool( + name = "elf_entity_memory_get", + description = "Fetch an entity-scoped memory view across attached core blocks and graph-linked archival notes.", + input_schema = entity_memory_get_schema() + )] + async fn elf_entity_memory_get( + &self, + mut params: JsonObject, + ) -> Result { + // read_profile is part of the MCP server configuration and is not client-controlled. + let _ = support::take_optional_string(&mut params, "read_profile")?; + + self.forward(HttpMethod::Get, "/v2/entity-memory", params, None).await + } + + #[rmcp::tool( + name = "elf_dreaming_review_queue", + description = "List source-backed Dreaming review queue proposals with variants, affected refs, lint flags, policy gates, and review audit.", + input_schema = dreaming_review_queue_schema() + )] + async fn elf_dreaming_review_queue( + &self, + params: JsonObject, + ) -> Result { + self.forward(HttpMethod::Get, "/v2/admin/dreaming/review-queue", params, None).await + } + + #[rmcp::tool( + name = "elf_recall_debug_panel", + description = "Build an agent-facing cross-layer recall/debug panel and deterministic recall_trace over memory traces, source documents, knowledge pages, graph facts, and Dreaming proposals.", + input_schema = recall_debug_panel_schema() + )] + pub(in crate::app::server) async fn elf_recall_debug_panel( + &self, + params: JsonObject, + ) -> Result { + support::reject_context_override_params(¶ms)?; + + self.forward(HttpMethod::Post, "/v2/recall-debug/panel", params, None).await + } + + #[rmcp::tool( + name = "elf_searches_create", + description = "Create a search session using quick-find or planned-search mode. Response includes optional trajectory_summary for staged retrieval progress.", + input_schema = searches_create_schema() + )] + async fn elf_searches_create( + &self, + mut params: JsonObject, + ) -> Result { + // read_profile is part of the MCP server configuration and is not client-controlled. + let _ = support::take_optional_string(&mut params, "read_profile")?; + + self.forward(HttpMethod::Post, "/v2/searches", params, None).await + } + + #[rmcp::tool( + name = "elf_searches_get", + description = "Fetch a search session index view by search_id, including optional trajectory_summary.", + input_schema = searches_get_schema() + )] + async fn elf_searches_get(&self, mut params: JsonObject) -> Result { + let search_id = support::take_required_string(&mut params, "search_id")?; + let path = format!("/v2/searches/{search_id}"); + + self.forward(HttpMethod::Get, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_searches_timeline", + description = "Build a timeline view from a search session.", + input_schema = searches_timeline_schema() + )] + async fn elf_searches_timeline( + &self, + mut params: JsonObject, + ) -> Result { + let search_id = support::take_required_string(&mut params, "search_id")?; + let path = format!("/v2/searches/{search_id}/timeline"); + + self.forward(HttpMethod::Get, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_searches_notes", + description = "Fetch note details for selected note_ids from a search session. l0/l1 strip evidence/source_ref; l2 returns full detail.", + input_schema = searches_notes_schema() + )] + async fn elf_searches_notes( + &self, + mut params: JsonObject, + ) -> Result { + let search_id = support::take_required_string(&mut params, "search_id")?; + let path = format!("/v2/searches/{search_id}/notes"); + + self.forward(HttpMethod::Post, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_notes_list", + description = "List notes in a tenant and project with optional filters.", + input_schema = notes_list_schema() + )] + async fn elf_notes_list(&self, params: JsonObject) -> Result { + self.forward(HttpMethod::Get, "/v2/notes", params, None).await + } + + #[rmcp::tool( + name = "elf_notes_get", + description = "Fetch a single note by note_id.", + input_schema = notes_get_schema() + )] + async fn elf_notes_get(&self, mut params: JsonObject) -> Result { + let note_id = support::take_required_string(&mut params, "note_id")?; + let path = format!("/v2/notes/{note_id}"); + + self.forward(HttpMethod::Get, &path, JsonObject::new(), None).await + } + + #[rmcp::tool( + name = "elf_notes_patch", + description = "Patch a note by note_id. Only provided fields are updated.", + input_schema = notes_patch_schema() + )] + async fn elf_notes_patch(&self, mut params: JsonObject) -> Result { + let note_id = support::take_required_string(&mut params, "note_id")?; + let path = format!("/v2/notes/{note_id}"); + + self.forward(HttpMethod::Patch, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_notes_delete", + description = "Delete a note by note_id.", + input_schema = notes_get_schema() + )] + async fn elf_notes_delete(&self, mut params: JsonObject) -> Result { + let note_id = support::take_required_string(&mut params, "note_id")?; + let path = format!("/v2/notes/{note_id}"); + + self.forward(HttpMethod::Delete, &path, JsonObject::new(), None).await + } + + #[rmcp::tool( + name = "elf_notes_publish", + description = "Publish a note from agent_private into a shared space (team_shared or org_shared).", + input_schema = notes_publish_schema() + )] + async fn elf_notes_publish(&self, mut params: JsonObject) -> Result { + let note_id = support::take_required_string(&mut params, "note_id")?; + let path = format!("/v2/notes/{note_id}/publish"); + + self.forward(HttpMethod::Post, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_notes_unpublish", + description = "Unpublish a shared note back into agent_private scope.", + input_schema = notes_unpublish_schema() + )] + async fn elf_notes_unpublish( + &self, + mut params: JsonObject, + ) -> Result { + let note_id = support::take_required_string(&mut params, "note_id")?; + let path = format!("/v2/notes/{note_id}/unpublish"); + + self.forward(HttpMethod::Post, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_space_grants_list", + description = "List sharing grants for a space (team_shared or org_shared).", + input_schema = space_grants_list_schema() + )] + async fn elf_space_grants_list( + &self, + mut params: JsonObject, + ) -> Result { + let space = support::take_required_string(&mut params, "space")?; + let path = format!("/v2/spaces/{space}/grants"); + + self.forward(HttpMethod::Get, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_space_grant_upsert", + description = "Upsert a sharing grant for a space (team_shared or org_shared).", + input_schema = space_grant_upsert_schema() + )] + async fn elf_space_grant_upsert( + &self, + mut params: JsonObject, + ) -> Result { + let space = support::take_required_string(&mut params, "space")?; + let path = format!("/v2/spaces/{space}/grants"); + + self.forward(HttpMethod::Post, &path, params, None).await + } + + #[rmcp::tool( + name = "elf_space_grant_revoke", + description = "Revoke a sharing grant for a space (team_shared or org_shared).", + input_schema = space_grant_revoke_schema() + )] + async fn elf_space_grant_revoke( + &self, + mut params: JsonObject, + ) -> Result { + let space = support::take_required_string(&mut params, "space")?; + let path = format!("/v2/spaces/{space}/grants/revoke"); + + self.forward(HttpMethod::Post, &path, params, None).await + } +}