55import asyncio
66import json
77import sys
8+ from pathlib import Path
89from typing import Any
910
1011import mcp .server .stdio
1718from quorum .providers import list_all_models_sync
1819from quorum .team import FourPhaseConsensusTeam
1920
21+ # Limits for file reading
22+ MAX_FILES = 10
23+ MAX_FILE_SIZE = 100_000 # 100KB per file
24+ MAX_TOTAL_CONTEXT = 500_000 # 500KB total
25+
2026# Method descriptions for the resource
2127METHOD_INFO = {
2228 "standard" : {
@@ -146,6 +152,11 @@ async def list_tools() -> list[types.Tool]:
146152 "default" : False ,
147153 "description" : "Return full discussion (all phases). Default: false (only synthesis)" ,
148154 },
155+ "files" : {
156+ "type" : "array" ,
157+ "items" : {"type" : "string" },
158+ "description" : "Absolute file paths to include as context (max 10 files, 100KB each)" ,
159+ },
149160 },
150161 "required" : ["question" , "models" ],
151162 },
@@ -183,12 +194,78 @@ async def _handle_list_models() -> list[types.TextContent]:
183194 return [types .TextContent (type = "text" , text = json .dumps (serializable , indent = 2 ))]
184195
185196
197+ def _read_files (file_paths : list [str ]) -> tuple [str , list [str ]]:
198+ """Read files and return formatted context string.
199+
200+ Args:
201+ file_paths: List of absolute file paths to read.
202+
203+ Returns:
204+ Tuple of (context_string, errors).
205+ """
206+ if len (file_paths ) > MAX_FILES :
207+ return "" , [f"Too many files: { len (file_paths )} > { MAX_FILES } " ]
208+
209+ context_parts = []
210+ errors = []
211+ total_size = 0
212+
213+ for path_str in file_paths :
214+ try :
215+ path = Path (path_str )
216+ if not path .is_absolute ():
217+ errors .append (f"Not absolute path: { path_str } " )
218+ continue
219+
220+ if not path .exists ():
221+ errors .append (f"File not found: { path_str } " )
222+ continue
223+
224+ if not path .is_file ():
225+ errors .append (f"Not a file: { path_str } " )
226+ continue
227+
228+ size = path .stat ().st_size
229+ if size > MAX_FILE_SIZE :
230+ errors .append (f"File too large ({ size } > { MAX_FILE_SIZE } ): { path_str } " )
231+ continue
232+
233+ if total_size + size > MAX_TOTAL_CONTEXT :
234+ errors .append (f"Total context limit reached, skipping: { path_str } " )
235+ continue
236+
237+ content = path .read_text (encoding = "utf-8" , errors = "replace" )
238+ total_size += len (content )
239+
240+ # Format with filename header
241+ context_parts .append (f"=== { path .name } ===\n { content } " )
242+
243+ except Exception as e :
244+ errors .append (f"Error reading { path_str } : { e } " )
245+
246+ context = "\n \n " .join (context_parts )
247+ return context , errors
248+
249+
186250async def _handle_discuss (args : dict [str , Any ]) -> list [types .TextContent ]:
187251 """Run a Quorum discussion."""
188252 question = args ["question" ]
189253 model_ids = args ["models" ]
190254 method = args .get ("method" , "standard" )
191255 full_output = args .get ("full_output" , False )
256+ file_paths = args .get ("files" , [])
257+
258+ # Read files if provided
259+ file_context = ""
260+ file_errors : list [str ] = []
261+ if file_paths :
262+ file_context , file_errors = _read_files (file_paths )
263+
264+ # Build full question with file context
265+ if file_context :
266+ full_question = f"Context files:\n \n { file_context } \n \n ---\n \n Question: { question } "
267+ else :
268+ full_question = question
192269
193270 # Initialize before try block so they're available in except
194271 synthesis = None
@@ -200,7 +277,7 @@ async def _handle_discuss(args: dict[str, Any]) -> list[types.TextContent]:
200277 method_override = method ,
201278 )
202279
203- async for msg in team .run_stream (question ):
280+ async for msg in team .run_stream (full_question ):
204281 if hasattr (msg , "__dict__" ):
205282 msg_dict = {
206283 "type" : type (msg ).__name__ ,
@@ -219,13 +296,17 @@ async def _handle_discuss(args: dict[str, Any]) -> list[types.TextContent]:
219296 # Compact: return only synthesis
220297 if synthesis :
221298 # Clean up synthesis for readability
222- compact_result = {
299+ compact_result : dict [ str , Any ] = {
223300 "consensus" : synthesis .get ("consensus" ),
224301 "synthesis" : synthesis .get ("synthesis" ),
225302 "differences" : synthesis .get ("differences" ),
226303 "method" : synthesis .get ("method" ),
227304 "models" : model_ids ,
228305 }
306+ if file_errors :
307+ compact_result ["file_errors" ] = file_errors
308+ if file_paths :
309+ compact_result ["files_included" ] = len (file_paths ) - len (file_errors )
229310 return [types .TextContent (type = "text" , text = json .dumps (compact_result , indent = 2 ))]
230311
231312 # Fallback if no synthesis (shouldn't happen)
@@ -275,7 +356,7 @@ async def _run_server() -> None:
275356 write ,
276357 InitializationOptions (
277358 server_name = "quorum" ,
278- server_version = "1.1.1 " ,
359+ server_version = "1.1.2 " ,
279360 capabilities = server .get_capabilities (
280361 notification_options = NotificationOptions (),
281362 experimental_capabilities = {},
0 commit comments