misukisu commited on
Commit
5f08738
·
verified ·
1 Parent(s): bd14f71

Delete src

Browse files
Files changed (4) hide show
  1. src/indexer.rs +0 -236
  2. src/main.rs +0 -77
  3. src/mcp.rs +0 -388
  4. src/parser.rs +0 -179
src/indexer.rs DELETED
@@ -1,236 +0,0 @@
1
- use crate::parser::ParsedDocument;
2
- use anyhow::{Context, Result};
3
- use chrono::Utc;
4
- use serde::{Deserialize, Serialize};
5
- use std::fs;
6
- use std::path::{Path, PathBuf};
7
- use std::sync::Arc;
8
- use tantivy::collector::TopDocs;
9
- use tantivy::query::{BooleanQuery, Occur, Query, QueryParser, TermQuery};
10
- use tantivy::schema::*;
11
- use tantivy::snippet::SnippetGenerator;
12
- use tantivy::{doc, Index, IndexReader, IndexWriter, ReloadPolicy, TantivyDocument, Term};
13
- use tokio::sync::RwLock;
14
-
15
- #[derive(Clone)]
16
- pub struct IndexSchemaFields {
17
- pub id: Field,
18
- pub collection: Field,
19
- pub path: Field,
20
- pub title: Field,
21
- pub content: Field,
22
- pub extension: Field,
23
- pub file_size: Field,
24
- pub indexed_at: Field,
25
- }
26
-
27
- #[derive(Serialize, Deserialize, Debug, Clone)]
28
- pub struct SearchHit {
29
- pub score: f32,
30
- pub collection: String,
31
- pub path: String,
32
- pub title: String,
33
- pub snippet: String,
34
- pub extension: String,
35
- pub file_size: u64,
36
- }
37
-
38
- #[derive(Serialize, Deserialize, Debug, Clone)]
39
- pub struct CollectionStats {
40
- pub collection_name: String,
41
- pub doc_count: u64,
42
- }
43
-
44
- #[derive(Serialize, Deserialize, Debug, Clone)]
45
- pub struct IndexStats {
46
- pub total_documents: u64,
47
- pub num_segments: usize,
48
- pub index_path: String,
49
- pub collections: Vec<CollectionStats>,
50
- }
51
-
52
- pub struct TantivyEngine {
53
- index: Index,
54
- reader: IndexReader,
55
- writer: Arc<RwLock<IndexWriter>>,
56
- fields: IndexSchemaFields,
57
- base_path: PathBuf,
58
- }
59
-
60
- impl TantivyEngine {
61
- pub fn new<P: AsRef<Path>>(base_path: P) -> Result<Self> {
62
- let path_buf = base_path.as_ref().to_path_buf();
63
- fs::create_dir_all(&path_buf)?;
64
-
65
- let mut schema_builder = Schema::builder();
66
- let id = schema_builder.add_text_field("id", STRING | STORED);
67
- let collection = schema_builder.add_text_field("collection", STRING | STORED | FAST);
68
- let path = schema_builder.add_text_field("path", STRING | STORED);
69
- let title = schema_builder.add_text_field("title", TEXT | STORED);
70
- let content = schema_builder.add_text_field("content", TEXT | STORED);
71
- let extension = schema_builder.add_text_field("extension", STRING | STORED);
72
- let file_size = schema_builder.add_u64_field("file_size", STORED | FAST);
73
- let indexed_at = schema_builder.add_i64_field("indexed_at", STORED | FAST);
74
-
75
- let schema = schema_builder.build();
76
-
77
- let index = Index::open_or_create(
78
- tantivy::directory::MmapDirectory::open(&path_buf)?,
79
- schema.clone(),
80
- )?;
81
-
82
- // 100MB memory budget for the writer buffer
83
- let writer = index.writer(100 * 1024 * 1024)?;
84
- let reader = index
85
- .reader_builder()
86
- .reload_policy(ReloadPolicy::OnCommitWithDelay)
87
- .try_into()?;
88
-
89
- let fields = IndexSchemaFields {
90
- id,
91
- collection,
92
- path,
93
- title,
94
- content,
95
- extension,
96
- file_size,
97
- indexed_at,
98
- };
99
-
100
- Ok(Self {
101
- index,
102
- reader,
103
- writer: Arc::new(RwLock::new(writer)),
104
- fields,
105
- base_path: path_buf,
106
- })
107
- }
108
-
109
- pub async fn add_documents(&self, collection_name: &str, docs: Vec<ParsedDocument>) -> Result<usize> {
110
- let count = docs.len();
111
- let writer = self.writer.write().await;
112
-
113
- for doc in docs {
114
- let mut tantivy_doc = TantivyDocument::default();
115
- let doc_id = uuid::Uuid::new_v4().to_string();
116
-
117
- tantivy_doc.add_text(self.fields.id, &doc_id);
118
- tantivy_doc.add_text(self.fields.collection, collection_name);
119
- tantivy_doc.add_text(self.fields.path, &doc.path);
120
- tantivy_doc.add_text(self.fields.title, &doc.title);
121
- tantivy_doc.add_text(self.fields.content, &doc.content);
122
- tantivy_doc.add_text(self.fields.extension, &doc.extension);
123
- tantivy_doc.add_u64(self.fields.file_size, doc.size_bytes);
124
- tantivy_doc.add_i64(self.fields.indexed_at, Utc::now().timestamp());
125
-
126
- writer.add_document(tantivy_doc)?;
127
- }
128
-
129
- // Commit and reload index reader
130
- let mut writer_guard = writer;
131
- writer_guard.commit()?;
132
- drop(writer_guard);
133
-
134
- self.reader.reload()?;
135
- Ok(count)
136
- }
137
-
138
- pub fn search(
139
- &self,
140
- query_str: &str,
141
- collection_filter: Option<&str>,
142
- limit: usize,
143
- ) -> Result<Vec<SearchHit>> {
144
- let searcher = self.reader.searcher();
145
- let query_parser = QueryParser::for_index(
146
- &self.index,
147
- vec![self.fields.title, self.fields.content],
148
- );
149
-
150
- let parsed_query = query_parser
151
- .parse_query(query_str)
152
- .context("Query parse error")?;
153
-
154
- let final_query: Box<dyn Query> = if let Some(coll) = collection_filter {
155
- let coll_term = Term::from_field_text(self.fields.collection, coll);
156
- let coll_query = TermQuery::new(coll_term, IndexRecordOption::Basic);
157
-
158
- Box::new(BooleanQuery::new(vec![
159
- (Occur::Must, parsed_query),
160
- (Occur::Must, Box::new(coll_query)),
161
- ]))
162
- } else {
163
- parsed_query
164
- };
165
-
166
- let top_docs = searcher.search(&final_query, &TopDocs::with_limit(limit))?;
167
- let mut snippet_generator =
168
- SnippetGenerator::create(&searcher, &*final_query, self.fields.content)?;
169
- snippet_generator.set_max_num_chars(250);
170
-
171
- let mut hits = Vec::with_capacity(top_docs.len());
172
-
173
- for (score, doc_address) in top_docs {
174
- let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?;
175
-
176
- let coll_val = retrieved_doc
177
- .get_first(self.fields.collection)
178
- .and_then(|v| v.as_str())
179
- .unwrap_or("default")
180
- .to_string();
181
-
182
- let path_val = retrieved_doc
183
- .get_first(self.fields.path)
184
- .and_then(|v| v.as_str())
185
- .unwrap_or("")
186
- .to_string();
187
-
188
- let title_val = retrieved_doc
189
- .get_first(self.fields.title)
190
- .and_then(|v| v.as_str())
191
- .unwrap_or("")
192
- .to_string();
193
-
194
- let ext_val = retrieved_doc
195
- .get_first(self.fields.extension)
196
- .and_then(|v| v.as_str())
197
- .unwrap_or("")
198
- .to_string();
199
-
200
- let size_val = retrieved_doc
201
- .get_first(self.fields.file_size)
202
- .and_then(|v| v.as_u64())
203
- .unwrap_or(0);
204
-
205
- let snippet = snippet_generator.snippet_from_doc(&retrieved_doc).to_html();
206
-
207
- hits.push(SearchHit {
208
- score,
209
- collection: coll_val,
210
- path: path_val,
211
- title: title_val,
212
- snippet,
213
- extension: ext_val,
214
- file_size: size_val,
215
- });
216
- }
217
-
218
- Ok(hits)
219
- }
220
-
221
- pub fn get_stats(&self) -> Result<IndexStats> {
222
- let searcher = self.reader.searcher();
223
- let total_docs = searcher.num_docs();
224
- let num_segments = searcher.segment_readers().len();
225
-
226
- Ok(IndexStats {
227
- total_documents: total_docs,
228
- num_segments,
229
- index_path: self.base_path.to_string_lossy().to_string(),
230
- collections: vec![CollectionStats {
231
- collection_name: "global".to_string(),
232
- doc_count: total_docs,
233
- }],
234
- })
235
- }
236
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/main.rs DELETED
@@ -1,77 +0,0 @@
1
- mod indexer;
2
- mod mcp;
3
- mod parser;
4
-
5
- use clap::Parser;
6
- use indexer::TantivyEngine;
7
- use mcp::McpServer;
8
- use std::path::PathBuf;
9
- use std::sync::Arc;
10
- use tracing::info;
11
-
12
- #[derive(Parser, Debug)]
13
- #[command(name = "rust-mcp-search")]
14
- #[command(about = "Ultra-high-performance Tantivy-backed Model Context Protocol server", long_about = None)]
15
- struct Args {
16
- /// Transport mode: 'stdio' or 'http'
17
- #[arg(short, long, env = "MCP_TRANSPORT", default_value = "stdio")]
18
- transport: String,
19
-
20
- /// HTTP host binding
21
- #[arg(long, env = "HOST", default_value = "0.0.0.0")]
22
- host: String,
23
-
24
- /// HTTP port binding (defaults to HF Spaces standard 7860)
25
- #[arg(short, long, env = "PORT", default_value = "7860")]
26
- port: u16,
27
-
28
- /// Tantivy index directory path
29
- #[arg(short, long, env = "DATA_DIR", default_value = "./data/index")]
30
- index_dir: PathBuf,
31
-
32
- /// Rayon thread pool concurrency limit (0 = auto-detect hardware concurrency)
33
- #[arg(long, env = "RAYON_NUM_THREADS", default_value = "0")]
34
- threads: usize,
35
- }
36
-
37
- #[tokio::main]
38
- async fn main() -> anyhow::Result<()> {
39
- let args = Args::parse();
40
-
41
- // Direct all logs strictly to stderr to prevent breaking the MCP STDIO JSON protocol
42
- tracing_subscriber::fmt()
43
- .with_writer(std::io::stderr)
44
- .with_env_filter(
45
- tracing_subscriber::EnvFilter::from_default_env()
46
- .add_directive(tracing::Level::INFO.into()),
47
- )
48
- .init();
49
-
50
- if args.threads > 0 {
51
- rayon::ThreadPoolBuilder::new()
52
- .num_threads(args.threads)
53
- .build_global()?;
54
- info!("Configured Rayon thread pool with {} threads", args.threads);
55
- }
56
-
57
- info!("Initializing Tantivy index engine at: {:?}", args.index_dir);
58
- let engine = Arc::new(TantivyEngine::new(&args.index_dir)?);
59
- let server = Arc::new(McpServer::new(engine));
60
-
61
- match args.transport.to_lowercase().as_str() {
62
- "http" | "sse" => {
63
- info!("Starting MCP HTTP/SSE transport...");
64
- server.run_http(&args.host, args.port).await?;
65
- }
66
- "stdio" => {
67
- info!("Starting MCP STDIO transport...");
68
- server.run_stdio().await?;
69
- }
70
- other => {
71
- eprintln!("Unknown transport '{}'. Expected 'stdio' or 'http'.", other);
72
- std::process::exit(1);
73
- }
74
- }
75
-
76
- Ok(())
77
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/mcp.rs DELETED
@@ -1,388 +0,0 @@
1
- use crate::indexer::TantivyEngine;
2
- use crate::parser::{DocumentParser, ParsedDocument};
3
- use anyhow::Result;
4
- use axum::{
5
- extract::State,
6
- http::{header, HeaderMap, HeaderValue},
7
- response::sse::{Event, KeepAlive, Sse},
8
- routing::{get, post},
9
- Json, Router,
10
- };
11
- use futures::stream::Stream;
12
- use rayon::prelude::*;
13
- use serde::{Deserialize, Serialize};
14
- use serde_json::{json, Value};
15
- use std::convert::Infallible;
16
- use std::path::Path;
17
- use std::sync::Arc;
18
- use std::time::Duration;
19
- use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
20
- use tokio_stream::StreamExt;
21
- use tracing::info;
22
- use walkdir::WalkDir;
23
-
24
- #[derive(Serialize, Deserialize, Debug, Clone)]
25
- pub struct JsonRpcRequest {
26
- pub jsonrpc: String,
27
- pub id: Option<Value>,
28
- pub method: String,
29
- pub params: Option<Value>,
30
- }
31
-
32
- #[derive(Serialize, Deserialize, Debug, Clone)]
33
- pub struct JsonRpcResponse {
34
- pub jsonrpc: String,
35
- #[serde(skip_serializing_if = "Option::is_none")]
36
- pub id: Option<Value>,
37
- #[serde(skip_serializing_if = "Option::is_none")]
38
- pub result: Option<Value>,
39
- #[serde(skip_serializing_if = "Option::is_none")]
40
- pub error: Option<JsonRpcError>,
41
- }
42
-
43
- #[derive(Serialize, Deserialize, Debug, Clone)]
44
- pub struct JsonRpcError {
45
- pub code: i64,
46
- pub message: String,
47
- #[serde(skip_serializing_if = "Option::is_none")]
48
- pub data: Option<Value>,
49
- }
50
-
51
- pub struct McpServer {
52
- engine: Arc<TantivyEngine>,
53
- }
54
-
55
- impl McpServer {
56
- pub fn new(engine: Arc<TantivyEngine>) -> Self {
57
- Self { engine }
58
- }
59
-
60
- pub async fn handle_request(&self, req: JsonRpcRequest) -> JsonRpcResponse {
61
- let req_id = req.id.clone();
62
-
63
- match req.method.as_str() {
64
- "initialize" => {
65
- let init_result = json!({
66
- "protocolVersion": "2024-11-05",
67
- "capabilities": {
68
- "tools": {
69
- "listChanged": true
70
- }
71
- },
72
- "serverInfo": {
73
- "name": "rust-mcp-search",
74
- "version": "0.1.0"
75
- }
76
- });
77
- Self::success_response(req_id, init_result)
78
- }
79
- "notifications/initialized" | "ping" => {
80
- Self::success_response(req_id, json!({}))
81
- }
82
- "tools/list" => {
83
- let tools = json!({
84
- "tools": [
85
- {
86
- "name": "parse_and_index",
87
- "description": "Recursively scans a directory and indexes plain text, Markdown, JSON, PDF, CSV, and DOCX files in parallel using Rayon and Tantivy.",
88
- "inputSchema": {
89
- "type": "object",
90
- "properties": {
91
- "directory_path": {
92
- "type": "string",
93
- "description": "Absolute or relative directory path to index"
94
- },
95
- "collection_name": {
96
- "type": "string",
97
- "description": "Collection namespace identifier (default: 'default')"
98
- }
99
- },
100
- "required": ["directory_path"]
101
- }
102
- },
103
- {
104
- "name": "search_documents",
105
- "description": "Full-text BM25 search over indexed documents with highlighted snippets and relevance scores.",
106
- "inputSchema": {
107
- "type": "object",
108
- "properties": {
109
- "query": {
110
- "type": "string",
111
- "description": "Tantivy BM25 search query"
112
- },
113
- "collection_name": {
114
- "type": "string",
115
- "description": "Filter results by collection"
116
- },
117
- "limit": {
118
- "type": "integer",
119
- "description": "Max hits to return (default: 10)"
120
- }
121
- },
122
- "required": ["query"]
123
- }
124
- },
125
- {
126
- "name": "extract_document_text",
127
- "description": "High-speed raw text extraction from a single file (TXT, MD, PDF, DOCX, CSV, JSON).",
128
- "inputSchema": {
129
- "type": "object",
130
- "properties": {
131
- "file_path": {
132
- "type": "string",
133
- "description": "Path to document"
134
- }
135
- },
136
- "required": ["file_path"]
137
- }
138
- },
139
- {
140
- "name": "get_index_stats",
141
- "description": "Returns indexed document counts, segment counts, active collections, and memory/storage status.",
142
- "inputSchema": {
143
- "type": "object",
144
- "properties": {}
145
- }
146
- }
147
- ]
148
- });
149
- Self::success_response(req_id, tools)
150
- }
151
- "tools/call" => {
152
- let params = req.params.unwrap_or_default();
153
- let tool_name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
154
- let args = params.get("arguments").cloned().unwrap_or(json!({}));
155
-
156
- match self.dispatch_tool(tool_name, args).await {
157
- Ok(tool_output) => Self::success_response(
158
- req_id,
159
- json!({
160
- "content": [
161
- {
162
- "type": "text",
163
- "text": tool_output
164
- }
165
- ],
166
- "isError": false
167
- }),
168
- ),
169
- Err(e) => Self::success_response(
170
- req_id,
171
- json!({
172
- "content": [
173
- {
174
- "type": "text",
175
- "text": format!("Error executing tool '{}': {}", tool_name, e)
176
- }
177
- ],
178
- "isError": true
179
- }),
180
- ),
181
- }
182
- }
183
- _ => Self::error_response(req_id, -32601, format!("Method '{}' not found", req.method)),
184
- }
185
- }
186
-
187
- async fn dispatch_tool(&self, name: &str, args: Value) -> Result<String> {
188
- match name {
189
- "parse_and_index" => {
190
- let dir = args
191
- .get("directory_path")
192
- .and_then(|v| v.as_str())
193
- .ok_or_else(|| anyhow::anyhow!("Missing 'directory_path' argument"))?;
194
- let collection = args
195
- .get("collection_name")
196
- .and_then(|v| v.as_str())
197
- .unwrap_or("default");
198
-
199
- let dir_path = Path::new(dir).to_path_buf();
200
- if !dir_path.exists() {
201
- return Err(anyhow::anyhow!("Directory does not exist: {:?}", dir_path));
202
- }
203
-
204
- let paths: Vec<_> = WalkDir::new(&dir_path)
205
- .into_iter()
206
- .filter_map(|e| e.ok())
207
- .filter(|e| e.file_type().is_file())
208
- .map(|e| e.into_path())
209
- .collect();
210
-
211
- let total_found = paths.len();
212
-
213
- let parsed_docs: Vec<ParsedDocument> = paths
214
- .par_iter()
215
- .filter_map(|path| match DocumentParser::parse_file(path) {
216
- Ok(doc) => Some(doc),
217
- Err(err) => {
218
- tracing::warn!("Skipping {:?}: {}", path, err);
219
- None
220
- }
221
- })
222
- .collect();
223
-
224
- let parsed_count = parsed_docs.len();
225
- let indexed_count = self.engine.add_documents(collection, parsed_docs).await?;
226
-
227
- Ok(json!({
228
- "status": "success",
229
- "collection": collection,
230
- "scanned_files": total_found,
231
- "parsed_documents": parsed_count,
232
- "indexed_documents": indexed_count,
233
- "source_path": dir
234
- })
235
- .to_string())
236
- }
237
- "search_documents" => {
238
- let query = args
239
- .get("query")
240
- .and_then(|v| v.as_str())
241
- .ok_or_else(|| anyhow::anyhow!("Missing 'query' argument"))?;
242
- let collection = args.get("collection_name").and_then(|v| v.as_str());
243
- let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
244
-
245
- let hits = self.engine.search(query, collection, limit)?;
246
- Ok(serde_json::to_string_pretty(&hits)?)
247
- }
248
- "extract_document_text" => {
249
- let path_str = args
250
- .get("file_path")
251
- .and_then(|v| v.as_str())
252
- .ok_or_else(|| anyhow::anyhow!("Missing 'file_path' argument"))?;
253
-
254
- let doc = DocumentParser::parse_file(path_str)?;
255
- Ok(json!({
256
- "path": doc.path,
257
- "title": doc.title,
258
- "extension": doc.extension,
259
- "size_bytes": doc.size_bytes,
260
- "content": doc.content
261
- })
262
- .to_string())
263
- }
264
- "get_index_stats" => {
265
- let stats = self.engine.get_stats()?;
266
- Ok(serde_json::to_string_pretty(&stats)?)
267
- }
268
- unknown => Err(anyhow::anyhow!("Unknown tool name: {}", unknown)),
269
- }
270
- }
271
-
272
- fn success_response(id: Option<Value>, result: Value) -> JsonRpcResponse {
273
- JsonRpcResponse {
274
- jsonrpc: "2.0".to_string(),
275
- id,
276
- result: Some(result),
277
- error: None,
278
- }
279
- }
280
-
281
- fn error_response(id: Option<Value>, code: i64, message: String) -> JsonRpcResponse {
282
- JsonRpcResponse {
283
- jsonrpc: "2.0".to_string(),
284
- id,
285
- result: None,
286
- error: Some(JsonRpcError {
287
- code,
288
- message,
289
- data: None,
290
- }),
291
- }
292
- }
293
-
294
- pub async fn run_stdio(self: Arc<Self>) -> Result<()> {
295
- let stdin = tokio::io::stdin();
296
- let mut reader = BufReader::new(stdin).lines();
297
- let mut stdout = tokio::io::stdout();
298
-
299
- while let Some(line) = reader.next_line().await? {
300
- if line.trim().is_empty() {
301
- continue;
302
- }
303
-
304
- match serde_json::from_str::<JsonRpcRequest>(&line) {
305
- Ok(request) => {
306
- let response = self.handle_request(request).await;
307
- let out_json = serde_json::to_string(&response)?;
308
- stdout.write_all(out_json.as_bytes()).await?;
309
- stdout.write_all(b"\n").await?;
310
- stdout.flush().await?;
311
- }
312
- Err(e) => {
313
- let err_resp = Self::error_response(None, -32700, format!("Parse error: {}", e));
314
- let out_json = serde_json::to_string(&err_resp)?;
315
- stdout.write_all(out_json.as_bytes()).await?;
316
- stdout.write_all(b"\n").await?;
317
- stdout.flush().await?;
318
- }
319
- }
320
- }
321
- Ok(())
322
- }
323
-
324
- pub async fn run_http(self: Arc<Self>, host: &str, port: u16) -> Result<()> {
325
- let app = Router::new()
326
- .route("/rpc", post(handle_http_rpc))
327
- .route("/sse", get(handle_sse))
328
- .route("/health", get(|| async { "healthy" }))
329
- .layer(tower_http::cors::CorsLayer::permissive())
330
- .with_state(self);
331
-
332
- let addr = format!("{}:{}", host, port);
333
- info!("MCP HTTP/SSE server listening on http://{}", addr);
334
- let listener = tokio::net::TcpListener::bind(&addr).await?;
335
- axum::serve(listener, app).await?;
336
- Ok(())
337
- }
338
- }
339
-
340
- async fn handle_http_rpc(
341
- State(server): State<Arc<McpServer>>,
342
- Json(payload): Json<JsonRpcRequest>,
343
- ) -> Json<JsonRpcResponse> {
344
- let resp = server.handle_request(payload).await;
345
- Json(resp)
346
- }
347
-
348
- async fn handle_sse(
349
- State(_server): State<Arc<McpServer>>,
350
- ) -> (HeaderMap, Sse<impl Stream<Item = Result<Event, Infallible>>>) {
351
- let mut headers = HeaderMap::new();
352
- headers.insert("x-accel-buffering", HeaderValue::from_static("no"));
353
- headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache, no-transform"));
354
- headers.insert(header::CONNECTION, HeaderValue::from_static("keep-alive"));
355
- headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/event-stream"));
356
-
357
- // Emit initial endpoint AND tool availability notification immediately
358
- let initial_events = vec![
359
- Ok(Event::default().event("endpoint").data("/rpc")),
360
- Ok(Event::default().event("message").data(
361
- json!({
362
- "jsonrpc": "2.0",
363
- "method": "notifications/tools/list_changed",
364
- "params": {}
365
- })
366
- .to_string(),
367
- )),
368
- ];
369
-
370
- let initial = tokio_stream::iter(initial_events);
371
-
372
- // 5-second heartbeats keep connection alive on Hugging Face Spaces proxy
373
- let interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
374
- Duration::from_secs(5),
375
- ))
376
- .map(|_| Ok(Event::default().comment("keep-alive")));
377
-
378
- let stream = initial.chain(interval);
379
-
380
- (
381
- headers,
382
- Sse::new(stream).keep_alive(
383
- KeepAlive::new()
384
- .interval(Duration::from_secs(5))
385
- .text("keep-alive"),
386
- ),
387
- )
388
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/parser.rs DELETED
@@ -1,179 +0,0 @@
1
- use anyhow::{anyhow, Context, Result};
2
- use memmap2::Mmap;
3
- use quick_xml::events::Event;
4
- use quick_xml::reader::Reader;
5
- use std::fs::File;
6
- use std::io::{Cursor, Read};
7
- use std::path::Path;
8
- use thiserror::Error;
9
-
10
- #[derive(Error, Debug)]
11
- pub enum ParserError {
12
- #[error("I/O error: {0}")]
13
- Io(#[from] std::io::Error),
14
- #[error("Unsupported file format: {0}")]
15
- UnsupportedFormat(String),
16
- #[error("Corrupt document: {0}")]
17
- CorruptDocument(String),
18
- }
19
-
20
- #[derive(Debug, Clone)]
21
- pub struct ParsedDocument {
22
- pub path: String,
23
- pub title: String,
24
- pub content: String,
25
- pub extension: String,
26
- pub size_bytes: u64,
27
- }
28
-
29
- pub struct DocumentParser;
30
-
31
- impl DocumentParser {
32
- pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<ParsedDocument> {
33
- let path_ref = path.as_ref();
34
- let metadata = std::fs::metadata(path_ref)
35
- .with_context(|| format!("Failed to read metadata for {:?}", path_ref))?;
36
- let size_bytes = metadata.len();
37
-
38
- if size_bytes == 0 {
39
- return Ok(ParsedDocument {
40
- path: path_ref.to_string_lossy().to_string(),
41
- title: Self::extract_title(path_ref),
42
- content: String::new(),
43
- extension: Self::get_extension(path_ref),
44
- size_bytes: 0,
45
- });
46
- }
47
-
48
- let extension = Self::get_extension(path_ref);
49
- let title = Self::extract_title(path_ref);
50
-
51
- let file = File::open(path_ref)?;
52
-
53
- let content = if size_bytes >= 16 * 1024 {
54
- let mmap = unsafe { Mmap::map(&file)? };
55
- Self::extract_content_from_bytes(&mmap, &extension)?
56
- } else {
57
- let mut buffer = Vec::with_capacity(size_bytes as usize);
58
- let mut reader = std::io::BufReader::new(file);
59
- reader.read_to_end(&mut buffer)?;
60
- Self::extract_content_from_bytes(&buffer, &extension)?
61
- };
62
-
63
- Ok(ParsedDocument {
64
- path: path_ref.to_string_lossy().to_string(),
65
- title,
66
- content,
67
- extension,
68
- size_bytes,
69
- })
70
- }
71
-
72
- fn extract_content_from_bytes(bytes: &[u8], extension: &str) -> Result<String> {
73
- match extension.to_lowercase().as_str() {
74
- "txt" | "md" | "markdown" | "log" | "rs" | "py" | "js" | "ts" | "toml" | "yaml" | "yml" => {
75
- match std::str::from_utf8(bytes) {
76
- Ok(valid_str) => Ok(valid_str.to_string()),
77
- Err(_) => Ok(String::from_utf8_lossy(bytes).into_owned()),
78
- }
79
- }
80
- "json" => {
81
- let val: serde_json::Value = serde_json::from_slice(bytes)
82
- .context("Invalid JSON document")?;
83
- if let Some(text) = val.as_str() {
84
- Ok(text.to_string())
85
- } else {
86
- Ok(serde_json::to_string_pretty(&val)?)
87
- }
88
- }
89
- "csv" => Self::parse_csv(bytes),
90
- "docx" => Self::parse_docx(bytes),
91
- "pdf" => Self::parse_pdf(bytes),
92
- ext => Err(ParserError::UnsupportedFormat(ext.to_string()).into()),
93
- }
94
- }
95
-
96
- fn parse_csv(bytes: &[u8]) -> Result<String> {
97
- let mut rdr = csv::ReaderBuilder::new()
98
- .flexible(true)
99
- .has_headers(true)
100
- .from_reader(Cursor::new(bytes));
101
-
102
- let mut output = String::with_capacity(bytes.len());
103
-
104
- if let Ok(headers) = rdr.headers() {
105
- output.push_str(&headers.iter().collect::<Vec<_>>().join(" | "));
106
- output.push('\n');
107
- }
108
-
109
- for result in rdr.records() {
110
- let record = result?;
111
- output.push_str(&record.iter().collect::<Vec<_>>().join(" | "));
112
- output.push('\n');
113
- }
114
-
115
- Ok(output)
116
- }
117
-
118
- fn parse_docx(bytes: &[u8]) -> Result<String> {
119
- let cursor = Cursor::new(bytes);
120
- let mut archive = zip::ZipArchive::new(cursor)
121
- .context("Failed to open DOCX as ZIP archive")?;
122
-
123
- let mut document_xml = archive
124
- .by_name("word/document.xml")
125
- .context("Missing word/document.xml inside DOCX archive")?;
126
-
127
- let mut xml_bytes = Vec::new();
128
- document_xml.read_to_end(&mut xml_bytes)?;
129
-
130
- let mut reader = Reader::from_reader(Cursor::new(xml_bytes));
131
- reader.config_mut().trim_text(true);
132
-
133
- let mut txt = String::new();
134
- let mut buf = Vec::new();
135
- let mut in_text_node = false;
136
-
137
- loop {
138
- match reader.read_event_into(&mut buf) {
139
- Ok(Event::Start(ref e)) if e.name().as_ref() == b"w:t" => {
140
- in_text_node = true;
141
- }
142
- Ok(Event::End(ref e)) if e.name().as_ref() == b"w:t" => {
143
- in_text_node = false;
144
- }
145
- Ok(Event::End(ref e)) if e.name().as_ref() == b"w:p" => {
146
- txt.push('\n');
147
- }
148
- Ok(Event::Text(e)) if in_text_node => {
149
- if let Ok(s) = e.unescape() {
150
- txt.push_str(&s);
151
- }
152
- }
153
- Ok(Event::Eof) => break,
154
- Err(e) => return Err(anyhow!("XML Parsing error in DOCX: {}", e)),
155
- _ => (),
156
- }
157
- buf.clear();
158
- }
159
-
160
- Ok(txt)
161
- }
162
-
163
- fn parse_pdf(bytes: &[u8]) -> Result<String> {
164
- pdf_extract::extract_text_from_mem(bytes)
165
- .context("Failed to extract text stream from PDF")
166
- }
167
-
168
- fn extract_title(path: &Path) -> String {
169
- path.file_name()
170
- .map(|f| f.to_string_lossy().to_string())
171
- .unwrap_or_else(|| "Untitled".to_string())
172
- }
173
-
174
- fn get_extension(path: &Path) -> String {
175
- path.extension()
176
- .map(|e| e.to_string_lossy().to_string())
177
- .unwrap_or_else(|| "txt".to_string())
178
- }
179
- }