Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .cursor/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"mcpServers": {
"quantlab-local": {
"command": "node",
"args": ["${workspaceFolder}/desktop/mcp-server.mjs"],
"env": {
"QUANTLAB_PROJECT_ROOT": "${workspaceFolder}"
}
}
}
}
34 changes: 34 additions & 0 deletions .cursor/rules/desktop-electron.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
description: QuantLab desktop Electron rules
globs:
- "desktop/**/*.js"
- "desktop/**/*.mjs"
- "desktop/**/*.cjs"
- "desktop/**/*.ts"
- "desktop/**/*.tsx"
- "desktop/**/*.jsx"
- "desktop/**/*.json"
alwaysApply: false
---

# Desktop Electron Rules

## Scope

- Keep Electron shell work inside `desktop/`.
- Treat the shell as a UI and orchestration layer, not a place for trading logic.
- Reuse the embedded `research_ui` surface rather than re-implementing it in the shell.

## Implementation

- Keep the main process and renderer responsibilities separate.
- Prefer small renderer modules over one large file.
- Preserve the command bus and the existing launch/run/compare/artifacts workflow.
- Keep the shell deterministic and operationally simple.

## Integration

- Do not bypass the Python CLI for core QuantLab behavior.
- Keep shell actions reversible and explicit.
- If the change touches launch or smoke flows, verify the existing npm scripts remain valid.

38 changes: 38 additions & 0 deletions .cursor/rules/python-quantlab.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
description: QuantLab Python backend rules
globs:
- "main.py"
- "src/**/*.py"
- "scripts/**/*.py"
- "test/**/*.py"
alwaysApply: false
---

# Python QuantLab Rules

## Boundaries

- Keep domain logic in `src/quantlab/`.
- Keep command routing in `src/quantlab/cli/`.
- Keep `main.py` as compatibility/bootstrap only.
- Keep generated artifacts in `outputs/`.

## Implementation

- Prefer small, reversible changes.
- Favor pure functions for analytics, metrics, and transforms.
- Use type hints for new Python code.
- Avoid side effects on import.
- Keep behavior deterministic where possible.

## Testing

- Add or update `pytest` coverage for any behavior change.
- Cover edge cases explicitly, including empty inputs, missing values, and no-op or resume paths.
- Do not silently change CLI or artifact contracts.

## Safety

- Treat broker and execution changes as safety-sensitive.
- Preserve explicit human gates, idempotency, and reconciliation logic.

30 changes: 30 additions & 0 deletions .cursor/rules/quantlab-core.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
description: QuantLab project-wide operating rules
alwaysApply: true
---

# QuantLab Cursor Rules

## Read First

Before making changes, read these context files:

- `@.agents/project-brief.md`
- `@.agents/implementation-rules.md`
- `@.agents/current-state.md`

## Change Discipline

- Make the smallest change that satisfies the task.
- Do not edit unrelated files.
- Do not move business logic into `main.py`.
- Prefer existing modules and patterns over new abstractions.
- Preserve backward compatibility unless the task explicitly changes a contract.

## Quality Rules

- Add or update `pytest` coverage for behavior changes.
- Keep deterministic behavior where possible.
- Preserve artifact schemas and CLI contracts unless the task requires a change.
- For broker or execution work, keep human gates explicit and preserve idempotency and reconciliation safety.

43 changes: 43 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# QuantLab Agent Guide

This repository uses explicit project context under `.agents/` and `.cursor/rules/`.
Read those files before making changes.
If Cursor is available, also honor `.cursor/mcp.json` and use the project MCP tools for validation before improvising ad hoc commands.

## Working Rules

- Keep changes small and scoped to the requested task.
- Do not edit unrelated files.
- Preserve the repository architecture:
- core logic in `src/quantlab/`
- CLI routing in `src/quantlab/cli/`
- desktop shell work in `desktop/`
- public docs in `docs/`
- generated artifacts in `outputs/`
- Keep `main.py` limited to compatibility/bootstrap behavior.
- Prefer reversible changes over broad refactors.
- Add or update `pytest` coverage when behavior changes.
- Treat broker and execution changes as safety-sensitive.
- Preserve deterministic behavior and artifact contracts unless the task explicitly changes them.
- Prefer the repo MCP tools for routine validation:
- `quantlab_check`
- `quantlab_version`
- `quantlab_runs_list`
- `quantlab_paper_sessions_health`
- `quantlab_desktop_smoke`

## Before Implementing

1. Read `.agents/project-brief.md`.
2. Read `.agents/implementation-rules.md`.
3. Read `.agents/current-state.md`.
4. Inspect `.cursor/rules/` and `.cursor/mcp.json` when working in this repo.
5. Inspect the exact files involved in the task.
6. Confirm existing behavior before changing it.

## Quality Bar

- Use type hints for new Python code.
- Avoid side effects on import.
- Keep documentation aligned with observable behavior.
- Do not introduce secrets or environment-specific data into version control.
15 changes: 15 additions & 0 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ cd desktop
npm run smoke
```

## Cursor MCP

This folder also exposes a local MCP server for Cursor via [`.cursor/mcp.json`](../.cursor/mcp.json).

Available tools:

- `quantlab_check`
- `quantlab_version`
- `quantlab_runs_list`
- `quantlab_paper_sessions_health`
- `quantlab_desktop_smoke`
- `quantlab_read_file`

The server entrypoint is `mcp-server.mjs`, and the `mcp` npm script runs it directly.

## Current Tabs

- Chat
Expand Down
196 changes: 196 additions & 0 deletions desktop/mcp-server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { spawn } from "node:child_process";
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DESKTOP_ROOT = __dirname;
const PROJECT_ROOT = path.resolve(DESKTOP_ROOT, "..");
const PYTHON_EXECUTABLE = process.env.QUANTLAB_PYTHON || "python";
const MAX_OUTPUT_CHARS = 12000;

function truncateText(text) {
if (text.length <= MAX_OUTPUT_CHARS) return text;
return `${text.slice(0, MAX_OUTPUT_CHARS)}\n...[truncated]`;
}

function runProcess(command, args, options = {}) {
return new Promise((resolve) => {
const child = spawn(command, args, {
cwd: options.cwd || PROJECT_ROOT,
env: { ...process.env, ...(options.env || {}) },
windowsHide: true,
shell: false,
});

let stdout = "";
let stderr = "";

child.stdout.on("data", (chunk) => {
stdout += String(chunk || "");
});
child.stderr.on("data", (chunk) => {
stderr += String(chunk || "");
});

const timeoutMs = options.timeoutMs || 120000;
const timer = setTimeout(() => {
child.kill();
resolve({
exitCode: 124,
stdout,
stderr: `${stderr}\nCommand timed out after ${timeoutMs}ms`.trim(),
});
}, timeoutMs);

child.on("error", (error) => {
clearTimeout(timer);
resolve({
exitCode: 1,
stdout,
stderr: error.message || String(error),
});
});

child.on("exit", (code) => {
clearTimeout(timer);
resolve({
exitCode: code ?? 1,
stdout,
stderr,
});
});
});
}

async function runPythonCli(args, timeoutMs = 120000) {
return runProcess(PYTHON_EXECUTABLE, ["main.py", ...args], {
cwd: PROJECT_ROOT,
timeoutMs,
});
}

async function formatProcessResult(label, result, commandLine) {
const sections = [
`Command: ${commandLine}`,
`Exit code: ${result.exitCode}`,
];
if (result.stdout.trim()) {
sections.push(`stdout:\n${truncateText(result.stdout.trim())}`);
}
if (result.stderr.trim()) {
sections.push(`stderr:\n${truncateText(result.stderr.trim())}`);
}
return {
content: [
{
type: "text",
text: `[${label}]\n${sections.join("\n\n")}\n`,
},
],
};
}

async function main() {
const server = new McpServer({
name: "quantlab-local",
version: "0.1.0",
});

server.registerTool("quantlab_check", {
description: "Run the standard QuantLab health check.",
}, async () => {
const result = await runPythonCli(["--check"], 120000);
return formatProcessResult("quantlab_check", result, "python main.py --check");
Comment on lines +107 to +108
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: The command string passed to formatProcessResult is hardcoded to python and can diverge from PYTHON_EXECUTABLE.

Because the display string is hardcoded as "python main.py --check" while the actual executable comes from PYTHON_EXECUTABLE, the shown command can differ from what was run (e.g. when QUANTLAB_PYTHON points to another interpreter). To keep them aligned, construct the display command from PYTHON_EXECUTABLE, e.g.:

const commandLine = `${PYTHON_EXECUTABLE} main.py --check`;
const result = await runPythonCli(["--check"], 120000);
return formatProcessResult("quantlab_check", result, commandLine);

You can apply the same approach to the other quantlab_* tools.

});

server.registerTool("quantlab_version", {
description: "Return the QuantLab CLI version.",
}, async () => {
const result = await runPythonCli(["--version"], 30000);
return formatProcessResult("quantlab_version", result, "python main.py --version");
});

server.registerTool("quantlab_runs_list", {
description: "List indexed QuantLab runs.",
}, async () => {
const result = await runPythonCli(["--runs-list"], 120000);
return formatProcessResult("quantlab_runs_list", result, "python main.py --runs-list");
});

server.registerTool("quantlab_paper_sessions_health", {
description: "Summarize the health of QuantLab paper sessions.",
}, async () => {
const result = await runPythonCli(["--paper-sessions-health"], 120000);
return formatProcessResult(
"quantlab_paper_sessions_health",
result,
"python main.py --paper-sessions-health",
);
});

server.registerTool("quantlab_desktop_smoke", {
description: "Run the QuantLab desktop smoke test.",
}, async () => {
const result = await runProcess("node", ["scripts/smoke.js"], {
cwd: DESKTOP_ROOT,
timeoutMs: 180000,
});
return formatProcessResult("quantlab_desktop_smoke", result, "node scripts/smoke.js");
});

server.registerTool("quantlab_read_file", {
description: "Read a text file within the QuantLab repository.",
inputSchema: {
relative_path: z.string().describe("Path relative to the QuantLab repository root"),
},
}, async ({ relative_path }) => {
Comment on lines +146 to +151
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): inputSchema for quantlab_read_file is not a Zod schema object, which may break validation and tool metadata.

Here inputSchema is a plain object instead of a Zod schema, so the MCP SDK may not treat it as a valid schema (breaking validation or metadata). This should be wrapped in z.object(...), e.g.

inputSchema: z.object({
  relative_path: z
    .string()
    .describe("Path relative to the QuantLab repository root"),
}),

and the handler signature adjusted if the SDK now passes an object matching that schema shape.

const resolvedPath = path.resolve(PROJECT_ROOT, relative_path);
const relative = path.relative(PROJECT_ROOT, resolvedPath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
return {
content: [
{
type: "text",
text: `Refusing to read outside the QuantLab repository: ${relative_path}`,
},
],
isError: true,
};
}

try {
const data = await fs.readFile(resolvedPath, "utf8");
return {
content: [
{
type: "text",
text: truncateText(data),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to read ${relative_path}: ${error.message || String(error)}`,
},
],
isError: true,
};
}
});

const transport = new StdioServerTransport();
await server.connect(transport);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
Loading
Loading