Skip to content

Commit 272e0e8

Browse files
committed
feat: v1.1.2 - MCP file context support
- Add 'files' parameter to quorum_discuss for passing file paths - MCP server reads files and includes them as context - Limits: max 10 files, 100KB per file, 500KB total - Auto GitHub Releases on tag push
1 parent 3a8451e commit 272e0e8

6 files changed

Lines changed: 105 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
---
1111

12+
## [1.1.2] - 2025-12-15
13+
14+
### Added
15+
16+
- **MCP File Context** - Pass files directly to discussions via `files` parameter
17+
- MCP server reads files and includes them as context
18+
- Limits: max 10 files, 100KB per file, 500KB total
19+
- Reports `files_included` count and `file_errors` in response
20+
21+
### Changed
22+
23+
- **Auto GitHub Releases** - Tags now automatically create GitHub Releases
24+
- Extracts changelog notes for the tagged version
25+
- Attaches `.whl` artifact to release
26+
27+
---
28+
1229
## [1.1.1] - 2025-12-14
1330

1431
### Fixed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Then in Claude:
5858
- `quorum_list_models` - List your configured models
5959

6060
**Features:**
61+
- Pass `files` parameter to include code/docs as context (max 10 files, 100KB each)
6162
- Reuses your existing `~/.quorum/.env` config - no duplicate API keys
6263
- Compact output by default (synthesis only) - saves context
6364
- Set `full_output: true` for complete discussion transcript

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "quorum-frontend",
3-
"version": "1.1.1",
3+
"version": "1.1.2",
44
"description": "Terminal UI for Quorum multi-agent consensus system",
55
"license": "BSL-1.1",
66
"type": "module",

frontend/src/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Box, Text } from "ink";
77
import { useStore } from "../store/index.js";
88
import { t } from "../i18n/index.js";
99

10-
const VERSION = "1.1.1";
10+
const VERSION = "1.1.2";
1111

1212
export function Header() {
1313
const { selectedModels, discussionMethod, availableModels } = useStore();

src/quorum/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# Version Information
88
# =============================================================================
99

10-
__version__ = "1.1.1"
10+
__version__ = "1.1.2"
1111
"""Quorum application version."""
1212

1313
PROTOCOL_VERSION = "1.0.0"

src/quorum/mcp/__init__.py

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
import json
77
import sys
8+
from pathlib import Path
89
from typing import Any
910

1011
import mcp.server.stdio
@@ -17,6 +18,11 @@
1718
from quorum.providers import list_all_models_sync
1819
from 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
2127
METHOD_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+
186250
async 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\nQuestion: {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

Comments
 (0)