Skip to content

Commit 0eb7364

Browse files
authored
Add Cursor rules and local MCP server (#253)
* feat(research-ui): surface pretrade artifact paths * Add Cursor rules and local MCP server
1 parent 9222ab6 commit 0eb7364

9 files changed

Lines changed: 1518 additions & 10 deletions

File tree

.cursor/mcp.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"mcpServers": {
3+
"quantlab-local": {
4+
"command": "node",
5+
"args": ["${workspaceFolder}/desktop/mcp-server.mjs"],
6+
"env": {
7+
"QUANTLAB_PROJECT_ROOT": "${workspaceFolder}"
8+
}
9+
}
10+
}
11+
}

.cursor/rules/desktop-electron.mdc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
description: QuantLab desktop Electron rules
3+
globs:
4+
- "desktop/**/*.js"
5+
- "desktop/**/*.mjs"
6+
- "desktop/**/*.cjs"
7+
- "desktop/**/*.ts"
8+
- "desktop/**/*.tsx"
9+
- "desktop/**/*.jsx"
10+
- "desktop/**/*.json"
11+
alwaysApply: false
12+
---
13+
14+
# Desktop Electron Rules
15+
16+
## Scope
17+
18+
- Keep Electron shell work inside `desktop/`.
19+
- Treat the shell as a UI and orchestration layer, not a place for trading logic.
20+
- Reuse the embedded `research_ui` surface rather than re-implementing it in the shell.
21+
22+
## Implementation
23+
24+
- Keep the main process and renderer responsibilities separate.
25+
- Prefer small renderer modules over one large file.
26+
- Preserve the command bus and the existing launch/run/compare/artifacts workflow.
27+
- Keep the shell deterministic and operationally simple.
28+
29+
## Integration
30+
31+
- Do not bypass the Python CLI for core QuantLab behavior.
32+
- Keep shell actions reversible and explicit.
33+
- If the change touches launch or smoke flows, verify the existing npm scripts remain valid.
34+

.cursor/rules/python-quantlab.mdc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
description: QuantLab Python backend rules
3+
globs:
4+
- "main.py"
5+
- "src/**/*.py"
6+
- "scripts/**/*.py"
7+
- "test/**/*.py"
8+
alwaysApply: false
9+
---
10+
11+
# Python QuantLab Rules
12+
13+
## Boundaries
14+
15+
- Keep domain logic in `src/quantlab/`.
16+
- Keep command routing in `src/quantlab/cli/`.
17+
- Keep `main.py` as compatibility/bootstrap only.
18+
- Keep generated artifacts in `outputs/`.
19+
20+
## Implementation
21+
22+
- Prefer small, reversible changes.
23+
- Favor pure functions for analytics, metrics, and transforms.
24+
- Use type hints for new Python code.
25+
- Avoid side effects on import.
26+
- Keep behavior deterministic where possible.
27+
28+
## Testing
29+
30+
- Add or update `pytest` coverage for any behavior change.
31+
- Cover edge cases explicitly, including empty inputs, missing values, and no-op or resume paths.
32+
- Do not silently change CLI or artifact contracts.
33+
34+
## Safety
35+
36+
- Treat broker and execution changes as safety-sensitive.
37+
- Preserve explicit human gates, idempotency, and reconciliation logic.
38+

.cursor/rules/quantlab-core.mdc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
description: QuantLab project-wide operating rules
3+
alwaysApply: true
4+
---
5+
6+
# QuantLab Cursor Rules
7+
8+
## Read First
9+
10+
Before making changes, read these context files:
11+
12+
- `@.agents/project-brief.md`
13+
- `@.agents/implementation-rules.md`
14+
- `@.agents/current-state.md`
15+
16+
## Change Discipline
17+
18+
- Make the smallest change that satisfies the task.
19+
- Do not edit unrelated files.
20+
- Do not move business logic into `main.py`.
21+
- Prefer existing modules and patterns over new abstractions.
22+
- Preserve backward compatibility unless the task explicitly changes a contract.
23+
24+
## Quality Rules
25+
26+
- Add or update `pytest` coverage for behavior changes.
27+
- Keep deterministic behavior where possible.
28+
- Preserve artifact schemas and CLI contracts unless the task requires a change.
29+
- For broker or execution work, keep human gates explicit and preserve idempotency and reconciliation safety.
30+

AGENTS.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# QuantLab Agent Guide
2+
3+
This repository uses explicit project context under `.agents/` and `.cursor/rules/`.
4+
Read those files before making changes.
5+
If Cursor is available, also honor `.cursor/mcp.json` and use the project MCP tools for validation before improvising ad hoc commands.
6+
7+
## Working Rules
8+
9+
- Keep changes small and scoped to the requested task.
10+
- Do not edit unrelated files.
11+
- Preserve the repository architecture:
12+
- core logic in `src/quantlab/`
13+
- CLI routing in `src/quantlab/cli/`
14+
- desktop shell work in `desktop/`
15+
- public docs in `docs/`
16+
- generated artifacts in `outputs/`
17+
- Keep `main.py` limited to compatibility/bootstrap behavior.
18+
- Prefer reversible changes over broad refactors.
19+
- Add or update `pytest` coverage when behavior changes.
20+
- Treat broker and execution changes as safety-sensitive.
21+
- Preserve deterministic behavior and artifact contracts unless the task explicitly changes them.
22+
- Prefer the repo MCP tools for routine validation:
23+
- `quantlab_check`
24+
- `quantlab_version`
25+
- `quantlab_runs_list`
26+
- `quantlab_paper_sessions_health`
27+
- `quantlab_desktop_smoke`
28+
29+
## Before Implementing
30+
31+
1. Read `.agents/project-brief.md`.
32+
2. Read `.agents/implementation-rules.md`.
33+
3. Read `.agents/current-state.md`.
34+
4. Inspect `.cursor/rules/` and `.cursor/mcp.json` when working in this repo.
35+
5. Inspect the exact files involved in the task.
36+
6. Confirm existing behavior before changing it.
37+
38+
## Quality Bar
39+
40+
- Use type hints for new Python code.
41+
- Avoid side effects on import.
42+
- Keep documentation aligned with observable behavior.
43+
- Do not introduce secrets or environment-specific data into version control.

desktop/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ cd desktop
3131
npm run smoke
3232
```
3333

34+
## Cursor MCP
35+
36+
This folder also exposes a local MCP server for Cursor via [`.cursor/mcp.json`](../.cursor/mcp.json).
37+
38+
Available tools:
39+
40+
- `quantlab_check`
41+
- `quantlab_version`
42+
- `quantlab_runs_list`
43+
- `quantlab_paper_sessions_health`
44+
- `quantlab_desktop_smoke`
45+
- `quantlab_read_file`
46+
47+
The server entrypoint is `mcp-server.mjs`, and the `mcp` npm script runs it directly.
48+
3449
## Current Tabs
3550

3651
- Chat

desktop/mcp-server.mjs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { spawn } from "node:child_process";
2+
import { promises as fs } from "node:fs";
3+
import path from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
6+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8+
import { z } from "zod";
9+
10+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
11+
const DESKTOP_ROOT = __dirname;
12+
const PROJECT_ROOT = path.resolve(DESKTOP_ROOT, "..");
13+
const PYTHON_EXECUTABLE = process.env.QUANTLAB_PYTHON || "python";
14+
const MAX_OUTPUT_CHARS = 12000;
15+
16+
function truncateText(text) {
17+
if (text.length <= MAX_OUTPUT_CHARS) return text;
18+
return `${text.slice(0, MAX_OUTPUT_CHARS)}\n...[truncated]`;
19+
}
20+
21+
function runProcess(command, args, options = {}) {
22+
return new Promise((resolve) => {
23+
const child = spawn(command, args, {
24+
cwd: options.cwd || PROJECT_ROOT,
25+
env: { ...process.env, ...(options.env || {}) },
26+
windowsHide: true,
27+
shell: false,
28+
});
29+
30+
let stdout = "";
31+
let stderr = "";
32+
33+
child.stdout.on("data", (chunk) => {
34+
stdout += String(chunk || "");
35+
});
36+
child.stderr.on("data", (chunk) => {
37+
stderr += String(chunk || "");
38+
});
39+
40+
const timeoutMs = options.timeoutMs || 120000;
41+
const timer = setTimeout(() => {
42+
child.kill();
43+
resolve({
44+
exitCode: 124,
45+
stdout,
46+
stderr: `${stderr}\nCommand timed out after ${timeoutMs}ms`.trim(),
47+
});
48+
}, timeoutMs);
49+
50+
child.on("error", (error) => {
51+
clearTimeout(timer);
52+
resolve({
53+
exitCode: 1,
54+
stdout,
55+
stderr: error.message || String(error),
56+
});
57+
});
58+
59+
child.on("exit", (code) => {
60+
clearTimeout(timer);
61+
resolve({
62+
exitCode: code ?? 1,
63+
stdout,
64+
stderr,
65+
});
66+
});
67+
});
68+
}
69+
70+
async function runPythonCli(args, timeoutMs = 120000) {
71+
return runProcess(PYTHON_EXECUTABLE, ["main.py", ...args], {
72+
cwd: PROJECT_ROOT,
73+
timeoutMs,
74+
});
75+
}
76+
77+
async function formatProcessResult(label, result, commandLine) {
78+
const sections = [
79+
`Command: ${commandLine}`,
80+
`Exit code: ${result.exitCode}`,
81+
];
82+
if (result.stdout.trim()) {
83+
sections.push(`stdout:\n${truncateText(result.stdout.trim())}`);
84+
}
85+
if (result.stderr.trim()) {
86+
sections.push(`stderr:\n${truncateText(result.stderr.trim())}`);
87+
}
88+
return {
89+
content: [
90+
{
91+
type: "text",
92+
text: `[${label}]\n${sections.join("\n\n")}\n`,
93+
},
94+
],
95+
};
96+
}
97+
98+
async function main() {
99+
const server = new McpServer({
100+
name: "quantlab-local",
101+
version: "0.1.0",
102+
});
103+
104+
server.registerTool("quantlab_check", {
105+
description: "Run the standard QuantLab health check.",
106+
}, async () => {
107+
const result = await runPythonCli(["--check"], 120000);
108+
return formatProcessResult("quantlab_check", result, "python main.py --check");
109+
});
110+
111+
server.registerTool("quantlab_version", {
112+
description: "Return the QuantLab CLI version.",
113+
}, async () => {
114+
const result = await runPythonCli(["--version"], 30000);
115+
return formatProcessResult("quantlab_version", result, "python main.py --version");
116+
});
117+
118+
server.registerTool("quantlab_runs_list", {
119+
description: "List indexed QuantLab runs.",
120+
}, async () => {
121+
const result = await runPythonCli(["--runs-list"], 120000);
122+
return formatProcessResult("quantlab_runs_list", result, "python main.py --runs-list");
123+
});
124+
125+
server.registerTool("quantlab_paper_sessions_health", {
126+
description: "Summarize the health of QuantLab paper sessions.",
127+
}, async () => {
128+
const result = await runPythonCli(["--paper-sessions-health"], 120000);
129+
return formatProcessResult(
130+
"quantlab_paper_sessions_health",
131+
result,
132+
"python main.py --paper-sessions-health",
133+
);
134+
});
135+
136+
server.registerTool("quantlab_desktop_smoke", {
137+
description: "Run the QuantLab desktop smoke test.",
138+
}, async () => {
139+
const result = await runProcess("node", ["scripts/smoke.js"], {
140+
cwd: DESKTOP_ROOT,
141+
timeoutMs: 180000,
142+
});
143+
return formatProcessResult("quantlab_desktop_smoke", result, "node scripts/smoke.js");
144+
});
145+
146+
server.registerTool("quantlab_read_file", {
147+
description: "Read a text file within the QuantLab repository.",
148+
inputSchema: {
149+
relative_path: z.string().describe("Path relative to the QuantLab repository root"),
150+
},
151+
}, async ({ relative_path }) => {
152+
const resolvedPath = path.resolve(PROJECT_ROOT, relative_path);
153+
const relative = path.relative(PROJECT_ROOT, resolvedPath);
154+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
155+
return {
156+
content: [
157+
{
158+
type: "text",
159+
text: `Refusing to read outside the QuantLab repository: ${relative_path}`,
160+
},
161+
],
162+
isError: true,
163+
};
164+
}
165+
166+
try {
167+
const data = await fs.readFile(resolvedPath, "utf8");
168+
return {
169+
content: [
170+
{
171+
type: "text",
172+
text: truncateText(data),
173+
},
174+
],
175+
};
176+
} catch (error) {
177+
return {
178+
content: [
179+
{
180+
type: "text",
181+
text: `Failed to read ${relative_path}: ${error.message || String(error)}`,
182+
},
183+
],
184+
isError: true,
185+
};
186+
}
187+
});
188+
189+
const transport = new StdioServerTransport();
190+
await server.connect(transport);
191+
}
192+
193+
main().catch((error) => {
194+
console.error(error);
195+
process.exit(1);
196+
});

0 commit comments

Comments
 (0)