-
Notifications
You must be signed in to change notification settings - Fork 1
Add Cursor rules and local MCP server #253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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}" | ||
| } | ||
| } | ||
| } | ||
| } |
| 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. | ||
|
|
| 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. | ||
|
|
| 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. | ||
|
|
| 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. |
| 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"); | ||
| }); | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Here 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); | ||
| }); | ||
There was a problem hiding this comment.
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
formatProcessResultis hardcoded topythonand can diverge fromPYTHON_EXECUTABLE.Because the display string is hardcoded as
"python main.py --check"while the actual executable comes fromPYTHON_EXECUTABLE, the shown command can differ from what was run (e.g. whenQUANTLAB_PYTHONpoints to another interpreter). To keep them aligned, construct the display command fromPYTHON_EXECUTABLE, e.g.:You can apply the same approach to the other
quantlab_*tools.