riskmodels 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -0
- package/dist/commands/agent.d.ts +2 -0
- package/dist/commands/agent.js +76 -0
- package/dist/commands/balance.d.ts +2 -0
- package/dist/commands/balance.js +25 -0
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +76 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +152 -0
- package/dist/commands/correlation.d.ts +2 -0
- package/dist/commands/correlation.js +80 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +51 -0
- package/dist/commands/estimate.d.ts +2 -0
- package/dist/commands/estimate.js +40 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +60 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +136 -0
- package/dist/commands/l3.d.ts +2 -0
- package/dist/commands/l3.js +33 -0
- package/dist/commands/macro-factors.d.ts +2 -0
- package/dist/commands/macro-factors.js +39 -0
- package/dist/commands/manifest.d.ts +2 -0
- package/dist/commands/manifest.js +270 -0
- package/dist/commands/mcp-config.d.ts +2 -0
- package/dist/commands/mcp-config.js +95 -0
- package/dist/commands/mcp.d.ts +12 -0
- package/dist/commands/mcp.js +59 -0
- package/dist/commands/metrics.d.ts +2 -0
- package/dist/commands/metrics.js +29 -0
- package/dist/commands/portfolio.d.ts +2 -0
- package/dist/commands/portfolio.js +80 -0
- package/dist/commands/query.d.ts +2 -0
- package/dist/commands/query.js +90 -0
- package/dist/commands/rankings.d.ts +2 -0
- package/dist/commands/rankings.js +108 -0
- package/dist/commands/returns.d.ts +2 -0
- package/dist/commands/returns.js +77 -0
- package/dist/commands/schema.d.ts +2 -0
- package/dist/commands/schema.js +77 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +25 -0
- package/dist/commands/tickers.d.ts +2 -0
- package/dist/commands/tickers.js +36 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +67 -0
- package/dist/lib/api-client.d.ts +22 -0
- package/dist/lib/api-client.js +100 -0
- package/dist/lib/api-url.d.ts +2 -0
- package/dist/lib/api-url.js +8 -0
- package/dist/lib/config.d.ts +22 -0
- package/dist/lib/config.js +46 -0
- package/dist/lib/credentials.d.ts +26 -0
- package/dist/lib/credentials.js +70 -0
- package/dist/lib/display.d.ts +1 -0
- package/dist/lib/display.js +19 -0
- package/dist/lib/mcp-config-paths.d.ts +16 -0
- package/dist/lib/mcp-config-paths.js +100 -0
- package/dist/lib/mcp-config-writer.d.ts +36 -0
- package/dist/lib/mcp-config-writer.js +265 -0
- package/dist/lib/mcp-install-plan.d.ts +16 -0
- package/dist/lib/mcp-install-plan.js +28 -0
- package/dist/lib/oauth.d.ts +2 -0
- package/dist/lib/oauth.js +47 -0
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +28 -0
- package/dist/lib/sql-validation.d.ts +9 -0
- package/dist/lib/sql-validation.js +21 -0
- package/package.json +41 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { loadConfig } from "../lib/config.js";
|
|
4
|
+
import { apiRootFromUserBase } from "../lib/api-url.js";
|
|
5
|
+
import { ApiHttpError, apiFetchOptionalAuth } from "../lib/api-client.js";
|
|
6
|
+
import { printResults } from "../lib/display.js";
|
|
7
|
+
/** Production health can exceed 15s when Supabase is cold; Vercel rollouts may return 502–504. */
|
|
8
|
+
const HEALTH_TIMEOUT_MS = 120_000;
|
|
9
|
+
const HEALTH_MAX_ATTEMPTS = 6;
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
function isTransientHealthFailure(err) {
|
|
14
|
+
if (err instanceof ApiHttpError) {
|
|
15
|
+
return [502, 503, 504].includes(err.status);
|
|
16
|
+
}
|
|
17
|
+
if (err instanceof Error) {
|
|
18
|
+
if (err.name === "AbortError" || err.name === "TimeoutError")
|
|
19
|
+
return true;
|
|
20
|
+
const m = err.message.toLowerCase();
|
|
21
|
+
if (m.includes("fetch failed"))
|
|
22
|
+
return true;
|
|
23
|
+
if (m.includes("econnreset") || m.includes("etimedout"))
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
export function healthCommand() {
|
|
29
|
+
return new Command("health")
|
|
30
|
+
.description("Service health check (GET /api/health, no auth). Retries transient 5xx and network errors.")
|
|
31
|
+
.action(async (_opts, cmd) => {
|
|
32
|
+
const json = cmd.optsWithGlobals().json ?? false;
|
|
33
|
+
const cfg = await loadConfig();
|
|
34
|
+
const apiRoot = apiRootFromUserBase(cfg?.apiBaseUrl);
|
|
35
|
+
for (let attempt = 1; attempt <= HEALTH_MAX_ATTEMPTS; attempt += 1) {
|
|
36
|
+
try {
|
|
37
|
+
const { body } = await apiFetchOptionalAuth(apiRoot, "GET", "/health", {
|
|
38
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
|
|
39
|
+
});
|
|
40
|
+
printResults(body, json);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
45
|
+
const retry = isTransientHealthFailure(e) && attempt < HEALTH_MAX_ATTEMPTS;
|
|
46
|
+
if (retry) {
|
|
47
|
+
const waitSec = 15 * attempt;
|
|
48
|
+
if (!json) {
|
|
49
|
+
console.error(chalk.yellow(`Health check attempt ${attempt}/${HEALTH_MAX_ATTEMPTS} failed (${msg}); retrying in ${waitSec}s…`));
|
|
50
|
+
}
|
|
51
|
+
await sleep(waitSec * 1000);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
console.error(chalk.red(msg));
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { configPath, DEFAULT_API_BASE, loadConfig } from "../lib/config.js";
|
|
5
|
+
import { detectClients, selectedClients } from "../lib/mcp-config-paths.js";
|
|
6
|
+
import { buildInstallPlans, firstPrompt } from "../lib/mcp-install-plan.js";
|
|
7
|
+
import { printResults } from "../lib/display.js";
|
|
8
|
+
import { redactSecret } from "../lib/redact.js";
|
|
9
|
+
import { installMcpConfig, writeSharedApiKey } from "../lib/mcp-config-writer.js";
|
|
10
|
+
import { apiRootFromUserBase } from "../lib/api-url.js";
|
|
11
|
+
import { apiFetchOptionalAuth } from "../lib/api-client.js";
|
|
12
|
+
function envApiKey() {
|
|
13
|
+
return process.env.RISKMODELS_API_KEY?.trim() || undefined;
|
|
14
|
+
}
|
|
15
|
+
async function connectionTest(apiBaseUrl) {
|
|
16
|
+
const apiRoot = apiRootFromUserBase(apiBaseUrl);
|
|
17
|
+
const endpoint = `${apiRoot.replace(/\/$/, "")}/health`;
|
|
18
|
+
try {
|
|
19
|
+
await apiFetchOptionalAuth(apiRoot, "GET", "/health", {
|
|
20
|
+
signal: AbortSignal.timeout(15_000),
|
|
21
|
+
});
|
|
22
|
+
return { ok: true, endpoint, message: "RiskModels API health check passed." };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
endpoint,
|
|
28
|
+
message: error instanceof Error ? error.message : String(error),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function resolveApiKey(opts) {
|
|
33
|
+
if (opts.apiKey?.trim())
|
|
34
|
+
return { apiKey: opts.apiKey.trim(), source: "--api-key" };
|
|
35
|
+
const envKey = envApiKey();
|
|
36
|
+
if (envKey)
|
|
37
|
+
return { apiKey: envKey, source: "RISKMODELS_API_KEY" };
|
|
38
|
+
const cfg = await loadConfig();
|
|
39
|
+
if (cfg?.apiKey?.trim())
|
|
40
|
+
return { apiKey: cfg.apiKey.trim(), source: configPath() };
|
|
41
|
+
if (opts.yes)
|
|
42
|
+
return { source: "missing" };
|
|
43
|
+
console.error(chalk.yellow("No RiskModels API key found."));
|
|
44
|
+
console.error(chalk.dim("Get a key: https://riskmodels.app/get-api-key"));
|
|
45
|
+
const answer = await inquirer.prompt([
|
|
46
|
+
{
|
|
47
|
+
type: "password",
|
|
48
|
+
name: "apiKey",
|
|
49
|
+
message: "RiskModels API key",
|
|
50
|
+
mask: "*",
|
|
51
|
+
validate: (value) => (value.trim() ? true : "API key is required"),
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
return { apiKey: answer.apiKey.trim(), source: "prompt" };
|
|
55
|
+
}
|
|
56
|
+
export function installCommand() {
|
|
57
|
+
return new Command("install")
|
|
58
|
+
.description("Detect AI clients and install/register the RiskModels MCP server")
|
|
59
|
+
.option("--client <name>", "claude | cursor | codex | vscode")
|
|
60
|
+
.option("--all", "Target all detected clients")
|
|
61
|
+
.option("--api-key <key>", "RiskModels API key for one-shot setup")
|
|
62
|
+
.option("--dry-run", "Show planned config changes without writing")
|
|
63
|
+
.option("--yes", "Non-interactive mode")
|
|
64
|
+
.option("--embed-key", "Explicitly embed the API key in MCP config env (not recommended)")
|
|
65
|
+
.option("--json", "JSON output")
|
|
66
|
+
.action(async (opts, cmd) => {
|
|
67
|
+
const json = opts.json || cmd.optsWithGlobals().json || false;
|
|
68
|
+
let clients;
|
|
69
|
+
try {
|
|
70
|
+
clients = selectedClients({ client: opts.client, all: opts.all });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const dryRun = opts.dryRun ?? false;
|
|
78
|
+
const { apiKey, source } = await resolveApiKey(opts);
|
|
79
|
+
const detections = await detectClients(clients);
|
|
80
|
+
const plans = buildInstallPlans(detections, { apiKey, embedKey: opts.embedKey });
|
|
81
|
+
const existingConfig = await loadConfig();
|
|
82
|
+
const output = {
|
|
83
|
+
dryRun,
|
|
84
|
+
apiKey: {
|
|
85
|
+
found: !!apiKey,
|
|
86
|
+
source,
|
|
87
|
+
value: redactSecret(apiKey),
|
|
88
|
+
storage: configPath(),
|
|
89
|
+
willStoreInSharedConfig: !!apiKey && source !== configPath() && !dryRun,
|
|
90
|
+
embeddedInMcpConfig: !!opts.embedKey,
|
|
91
|
+
},
|
|
92
|
+
clients: plans,
|
|
93
|
+
firstPrompt: firstPrompt(),
|
|
94
|
+
nextStep: dryRun
|
|
95
|
+
? "Review this plan, then rerun without --dry-run to write configs with backups."
|
|
96
|
+
: "Install complete. Restart your MCP client, then try the first prompt.",
|
|
97
|
+
};
|
|
98
|
+
if (dryRun) {
|
|
99
|
+
printResults(output, json);
|
|
100
|
+
if (!json) {
|
|
101
|
+
console.error(chalk.green(`First prompt to try: "${firstPrompt()}"`));
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!apiKey) {
|
|
106
|
+
printResults({
|
|
107
|
+
...output,
|
|
108
|
+
error: "No RiskModels API key found. Get one at https://riskmodels.app/get-api-key or pass --api-key.",
|
|
109
|
+
}, json);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const sharedConfigWrite = await writeSharedApiKey(apiKey, existingConfig?.apiBaseUrl ?? DEFAULT_API_BASE);
|
|
114
|
+
const writes = await Promise.all(detections.map((detection) => installMcpConfig(detection, {
|
|
115
|
+
apiKey,
|
|
116
|
+
embedKey: opts.embedKey,
|
|
117
|
+
apiBaseUrl: existingConfig?.apiBaseUrl,
|
|
118
|
+
})));
|
|
119
|
+
const test = await connectionTest(existingConfig?.apiBaseUrl);
|
|
120
|
+
const result = {
|
|
121
|
+
...output,
|
|
122
|
+
sharedConfigWrite,
|
|
123
|
+
writes,
|
|
124
|
+
connectionTest: test,
|
|
125
|
+
};
|
|
126
|
+
printResults(result, json);
|
|
127
|
+
if (writes.some((write) => write.action === "error") || !test.ok) {
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!json) {
|
|
132
|
+
console.error(chalk.green("RiskModels MCP install completed with backups."));
|
|
133
|
+
console.error(chalk.green(`First prompt to try: "${firstPrompt()}"`));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { loadConfig } from "../lib/config.js";
|
|
4
|
+
import { requireResolvedAuth } from "../lib/credentials.js";
|
|
5
|
+
import { apiFetchJson } from "../lib/api-client.js";
|
|
6
|
+
import { printResults } from "../lib/display.js";
|
|
7
|
+
export function l3Command() {
|
|
8
|
+
return new Command("l3")
|
|
9
|
+
.description("L3 factor decomposition time series (GET /l3-decomposition)")
|
|
10
|
+
.argument("<ticker>", "Symbol")
|
|
11
|
+
.option("--market-factor-etf <sym>", "Market factor ETF (default SPY)", "SPY")
|
|
12
|
+
.action(async (ticker, opts, cmd) => {
|
|
13
|
+
const json = cmd.optsWithGlobals().json ?? false;
|
|
14
|
+
const cfg = await loadConfig();
|
|
15
|
+
const auth = requireResolvedAuth(cfg, chalk.yellow);
|
|
16
|
+
if (!auth)
|
|
17
|
+
return;
|
|
18
|
+
const query = {
|
|
19
|
+
ticker: ticker.trim(),
|
|
20
|
+
market_factor_etf: opts.marketFactorEtf ?? "SPY",
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
const { body, costUsd } = await apiFetchJson(auth, "GET", "/l3-decomposition", { query });
|
|
24
|
+
printResults(body, json);
|
|
25
|
+
if (!json && costUsd)
|
|
26
|
+
console.log(chalk.dim(`Cost: $${costUsd}`));
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { loadConfig } from "../lib/config.js";
|
|
4
|
+
import { requireResolvedAuth } from "../lib/credentials.js";
|
|
5
|
+
import { apiFetchJson } from "../lib/api-client.js";
|
|
6
|
+
import { printResults } from "../lib/display.js";
|
|
7
|
+
export function macroFactorsCommand() {
|
|
8
|
+
return new Command("macro-factors")
|
|
9
|
+
.description("Daily macro factor returns from macro_factors (GET /macro-factors, no ticker)")
|
|
10
|
+
.option("--factors <list>", "Comma-separated factor keys (default: all six)")
|
|
11
|
+
.option("--start <date>", "Inclusive YYYY-MM-DD")
|
|
12
|
+
.option("--end <date>", "Inclusive YYYY-MM-DD")
|
|
13
|
+
.action(async (opts, cmd) => {
|
|
14
|
+
const json = cmd.optsWithGlobals().json ?? false;
|
|
15
|
+
const cfg = await loadConfig();
|
|
16
|
+
const auth = requireResolvedAuth(cfg, chalk.yellow);
|
|
17
|
+
if (!auth)
|
|
18
|
+
return;
|
|
19
|
+
const query = {};
|
|
20
|
+
if (opts.factors?.trim())
|
|
21
|
+
query.factors = opts.factors.trim();
|
|
22
|
+
if (opts.start?.trim())
|
|
23
|
+
query.start = opts.start.trim();
|
|
24
|
+
if (opts.end?.trim())
|
|
25
|
+
query.end = opts.end.trim();
|
|
26
|
+
try {
|
|
27
|
+
const { body, costUsd } = await apiFetchJson(auth, "GET", "/macro-factors", {
|
|
28
|
+
query,
|
|
29
|
+
});
|
|
30
|
+
printResults(body, json);
|
|
31
|
+
if (!json && costUsd)
|
|
32
|
+
console.log(chalk.dim(`Cost: $${costUsd}`));
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
/** Aligned with OpenAPI paths under /api (see OPENAPI_SPEC.yaml). */
|
|
4
|
+
const TOOLS = [
|
|
5
|
+
{
|
|
6
|
+
name: "riskmodels_cli_query",
|
|
7
|
+
description: "Execute read-only SQL SELECT against RiskModels data (billed). POST /api/cli/query with Bearer token.",
|
|
8
|
+
method: "POST",
|
|
9
|
+
path: "/api/cli/query",
|
|
10
|
+
properties: {
|
|
11
|
+
sql: { type: "string", description: "SELECT query (single statement, no semicolons)" },
|
|
12
|
+
limit: { type: "integer", description: "Max rows if SQL omits LIMIT (1–10000)" },
|
|
13
|
+
},
|
|
14
|
+
required: ["sql"],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "riskmodels_get_metrics",
|
|
18
|
+
description: "Latest V3 hedge ratios, explained risk, volatility. GET /api/metrics/{ticker}",
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/api/metrics/{ticker}",
|
|
21
|
+
properties: {
|
|
22
|
+
ticker: { type: "string", description: "Stock symbol, e.g. AAPL" },
|
|
23
|
+
},
|
|
24
|
+
required: ["ticker"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "riskmodels_batch_analyze",
|
|
28
|
+
description: "Multi-ticker batch: returns, hedge_ratios, full_metrics, l3_decomposition. POST /api/batch/analyze",
|
|
29
|
+
method: "POST",
|
|
30
|
+
path: "/api/batch/analyze",
|
|
31
|
+
properties: {
|
|
32
|
+
tickers: {
|
|
33
|
+
type: "array",
|
|
34
|
+
description: "Up to 100 ticker strings",
|
|
35
|
+
},
|
|
36
|
+
metrics: {
|
|
37
|
+
type: "array",
|
|
38
|
+
description: "Whitelist: returns, l3_decomposition, hedge_ratios, full_metrics",
|
|
39
|
+
},
|
|
40
|
+
years: { type: "integer", description: "1–15 for returns / l3_decomposition" },
|
|
41
|
+
format: { type: "string", description: "json | parquet | csv", enum: ["json", "parquet", "csv"] },
|
|
42
|
+
},
|
|
43
|
+
required: ["tickers", "metrics"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "riskmodels_portfolio_risk_index",
|
|
47
|
+
description: "Holdings-weighted L3 portfolio decomposition. POST /api/portfolio/risk-index",
|
|
48
|
+
method: "POST",
|
|
49
|
+
path: "/api/portfolio/risk-index",
|
|
50
|
+
properties: {
|
|
51
|
+
positions: {
|
|
52
|
+
type: "array",
|
|
53
|
+
description: "{ ticker, weight }[] — weights normalized server-side",
|
|
54
|
+
},
|
|
55
|
+
timeSeries: { type: "boolean", description: "Include daily portfolio ER time series" },
|
|
56
|
+
years: { type: "integer", description: "History when timeSeries is true" },
|
|
57
|
+
},
|
|
58
|
+
required: ["positions"],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "riskmodels_ticker_returns",
|
|
62
|
+
description: "Daily returns for a stock or ETF. Stocks include L1/L2/L3 hedge ratio + explained-risk columns; ETFs return date/returns_gross/price_close (L* columns are null). GET /api/ticker-returns",
|
|
63
|
+
method: "GET",
|
|
64
|
+
path: "/api/ticker-returns",
|
|
65
|
+
properties: {
|
|
66
|
+
ticker: { type: "string", description: "Stock or ETF symbol (e.g. NVDA, SPY)" },
|
|
67
|
+
years: { type: "integer", description: "Years of history (1–15)" },
|
|
68
|
+
format: { type: "string", description: "json | parquet | csv", enum: ["json", "parquet", "csv"] },
|
|
69
|
+
},
|
|
70
|
+
required: ["ticker"],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "riskmodels_l3_decomposition",
|
|
74
|
+
description: "L3 HR/ER time series. GET /api/l3-decomposition",
|
|
75
|
+
method: "GET",
|
|
76
|
+
path: "/api/l3-decomposition",
|
|
77
|
+
properties: {
|
|
78
|
+
ticker: { type: "string", description: "Stock symbol" },
|
|
79
|
+
market_factor_etf: { type: "string", description: "Default SPY" },
|
|
80
|
+
},
|
|
81
|
+
required: ["ticker"],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "riskmodels_factor_correlation",
|
|
85
|
+
description: "Macro factor correlation (batch or single). POST /api/correlation",
|
|
86
|
+
method: "POST",
|
|
87
|
+
path: "/api/correlation",
|
|
88
|
+
properties: {
|
|
89
|
+
ticker: { type: "string", description: "One ticker or use array in wire JSON for batch" },
|
|
90
|
+
factors: { type: "array", description: "Macro factor keys" },
|
|
91
|
+
return_type: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "gross | l1 | l2 | l3_residual",
|
|
94
|
+
enum: ["gross", "l1", "l2", "l3_residual"],
|
|
95
|
+
},
|
|
96
|
+
window_days: { type: "integer", description: "20–2000" },
|
|
97
|
+
method: { type: "string", description: "pearson | spearman", enum: ["pearson", "spearman"] },
|
|
98
|
+
},
|
|
99
|
+
required: ["ticker"],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "riskmodels_factor_correlation_by_ticker",
|
|
103
|
+
description: "GET convenience for macro correlations. GET /api/metrics/{ticker}/correlation",
|
|
104
|
+
method: "GET",
|
|
105
|
+
path: "/api/metrics/{ticker}/correlation",
|
|
106
|
+
properties: {
|
|
107
|
+
ticker: { type: "string", description: "Stock symbol" },
|
|
108
|
+
factors: { type: "string", description: "Comma-separated factor keys" },
|
|
109
|
+
},
|
|
110
|
+
required: ["ticker"],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "riskmodels_macro_factor_series",
|
|
114
|
+
description: "Raw macro factor daily returns. GET /api/macro-factors",
|
|
115
|
+
method: "GET",
|
|
116
|
+
path: "/api/macro-factors",
|
|
117
|
+
properties: {
|
|
118
|
+
factors: { type: "string", description: "Comma-separated factor keys" },
|
|
119
|
+
start: { type: "string", description: "YYYY-MM-DD" },
|
|
120
|
+
end: { type: "string", description: "YYYY-MM-DD" },
|
|
121
|
+
},
|
|
122
|
+
required: [],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "riskmodels_rankings_snapshot",
|
|
126
|
+
description: "Cross-sectional ranks for one ticker. GET /api/rankings/{ticker}",
|
|
127
|
+
method: "GET",
|
|
128
|
+
path: "/api/rankings/{ticker}",
|
|
129
|
+
properties: {
|
|
130
|
+
ticker: { type: "string", description: "Stock symbol" },
|
|
131
|
+
metric: { type: "string", description: "Optional filter" },
|
|
132
|
+
cohort: { type: "string", description: "universe | sector | subsector" },
|
|
133
|
+
window: { type: "string", description: "1d | 21d | 63d | 252d" },
|
|
134
|
+
},
|
|
135
|
+
required: ["ticker"],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "riskmodels_rankings_top",
|
|
139
|
+
description: "Leaderboard. GET /api/rankings/top",
|
|
140
|
+
method: "GET",
|
|
141
|
+
path: "/api/rankings/top",
|
|
142
|
+
properties: {
|
|
143
|
+
metric: { type: "string", description: "Required" },
|
|
144
|
+
cohort: { type: "string", description: "universe | sector | subsector" },
|
|
145
|
+
window: { type: "string", description: "1d | 21d | 63d | 252d" },
|
|
146
|
+
limit: { type: "integer", description: "1–100" },
|
|
147
|
+
},
|
|
148
|
+
required: ["metric", "cohort", "window"],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "riskmodels_search_tickers",
|
|
152
|
+
description: "Universe search (no auth). GET /api/tickers",
|
|
153
|
+
method: "GET",
|
|
154
|
+
path: "/api/tickers",
|
|
155
|
+
properties: {
|
|
156
|
+
search: { type: "string", description: "Symbol or name" },
|
|
157
|
+
mag7: { type: "boolean", description: "MAG7 only" },
|
|
158
|
+
include_metadata: { type: "boolean", description: "Sector / ETF metadata" },
|
|
159
|
+
},
|
|
160
|
+
required: [],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "riskmodels_health",
|
|
164
|
+
description: "Service health (no auth). GET /api/health",
|
|
165
|
+
method: "GET",
|
|
166
|
+
path: "/api/health",
|
|
167
|
+
properties: {},
|
|
168
|
+
required: [],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "riskmodels_estimate",
|
|
172
|
+
description: "Pre-flight cost estimate. POST /api/estimate",
|
|
173
|
+
method: "POST",
|
|
174
|
+
path: "/api/estimate",
|
|
175
|
+
properties: {
|
|
176
|
+
endpoint: { type: "string", description: "Endpoint slug, e.g. ticker-returns" },
|
|
177
|
+
params: { type: "string", description: "JSON object string with same params as the target endpoint" },
|
|
178
|
+
},
|
|
179
|
+
required: ["endpoint"],
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
function inputSchema(tool) {
|
|
183
|
+
const properties = {};
|
|
184
|
+
for (const [key, spec] of Object.entries(tool.properties)) {
|
|
185
|
+
const base = {
|
|
186
|
+
description: spec.description,
|
|
187
|
+
...(spec.enum ? { enum: spec.enum } : {}),
|
|
188
|
+
};
|
|
189
|
+
if (spec.type === "integer") {
|
|
190
|
+
base.type = "integer";
|
|
191
|
+
}
|
|
192
|
+
else if (spec.type === "array") {
|
|
193
|
+
base.type = "array";
|
|
194
|
+
base.items = { type: "string" };
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
base.type = spec.type;
|
|
198
|
+
}
|
|
199
|
+
properties[key] = base;
|
|
200
|
+
}
|
|
201
|
+
const schema = {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties,
|
|
204
|
+
};
|
|
205
|
+
if (tool.required.length) {
|
|
206
|
+
schema.required = tool.required;
|
|
207
|
+
}
|
|
208
|
+
return schema;
|
|
209
|
+
}
|
|
210
|
+
function anthropicManifest() {
|
|
211
|
+
return {
|
|
212
|
+
manifest_version: "1.0",
|
|
213
|
+
tools: TOOLS.map((t) => ({
|
|
214
|
+
name: t.name,
|
|
215
|
+
description: `${t.description} Base URL: https://riskmodels.app`,
|
|
216
|
+
input_schema: inputSchema(t),
|
|
217
|
+
})),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function openaiManifest() {
|
|
221
|
+
return {
|
|
222
|
+
tools: TOOLS.map((t) => ({
|
|
223
|
+
type: "function",
|
|
224
|
+
function: {
|
|
225
|
+
name: t.name,
|
|
226
|
+
description: `${t.description} Base URL: https://riskmodels.app`,
|
|
227
|
+
parameters: inputSchema(t),
|
|
228
|
+
},
|
|
229
|
+
})),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** Zed-oriented bundle: tool list + JSON Schema parameters (no auth embedded). */
|
|
233
|
+
function zedManifest() {
|
|
234
|
+
return {
|
|
235
|
+
schema_version: 1,
|
|
236
|
+
name: "riskmodels",
|
|
237
|
+
description: "RiskModels API tools (use Authorization: Bearer rm_agent_* or OAuth against https://riskmodels.app/api)",
|
|
238
|
+
tools: TOOLS.map((t) => ({
|
|
239
|
+
name: t.name,
|
|
240
|
+
description: t.description,
|
|
241
|
+
method: t.method,
|
|
242
|
+
path: t.path,
|
|
243
|
+
parameters: inputSchema(t),
|
|
244
|
+
})),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
export function manifestCommand() {
|
|
248
|
+
return new Command("manifest")
|
|
249
|
+
.description("Print static agent tool manifest (no API credentials required)")
|
|
250
|
+
.option("-f, --format <name>", "openai | anthropic | zed", "anthropic")
|
|
251
|
+
.action((opts) => {
|
|
252
|
+
const fmt = (opts.format ?? "anthropic").toLowerCase();
|
|
253
|
+
let out;
|
|
254
|
+
if (fmt === "openai") {
|
|
255
|
+
out = openaiManifest();
|
|
256
|
+
}
|
|
257
|
+
else if (fmt === "anthropic") {
|
|
258
|
+
out = anthropicManifest();
|
|
259
|
+
}
|
|
260
|
+
else if (fmt === "zed") {
|
|
261
|
+
out = zedManifest();
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
console.error(chalk.red(`Unknown format: ${fmt}. Use openai, anthropic, or zed.`));
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
console.log(JSON.stringify(out, null, 2));
|
|
269
|
+
});
|
|
270
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { loadConfig, configPath } from "../lib/config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Default absolute path users are most likely to paste into MCP configs.
|
|
6
|
+
* We don't try to introspect where the user cloned RiskModels_API — we print
|
|
7
|
+
* a placeholder they can edit. Override with --mcp-server-path for precision.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_MCP_SERVER_PLACEHOLDER = "/ABSOLUTE/PATH/TO/RiskModels_API/mcp/dist/index.js";
|
|
10
|
+
function claudeDesktopConfig(serverPath, apiKey) {
|
|
11
|
+
const env = {};
|
|
12
|
+
if (apiKey)
|
|
13
|
+
env.RISKMODELS_API_KEY = apiKey;
|
|
14
|
+
return {
|
|
15
|
+
mcpServers: {
|
|
16
|
+
riskmodels: {
|
|
17
|
+
command: "node",
|
|
18
|
+
args: [serverPath],
|
|
19
|
+
...(Object.keys(env).length ? { env } : {}),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function cursorConfig(serverPath, apiKey) {
|
|
25
|
+
// Same schema shape as Claude Desktop; Cursor reads .cursor/mcp.json
|
|
26
|
+
// either at workspace root or ~/.cursor/mcp.json globally.
|
|
27
|
+
return claudeDesktopConfig(serverPath, apiKey);
|
|
28
|
+
}
|
|
29
|
+
function zedConfig(serverPath, apiKey) {
|
|
30
|
+
const env = {};
|
|
31
|
+
if (apiKey)
|
|
32
|
+
env.RISKMODELS_API_KEY = apiKey;
|
|
33
|
+
return {
|
|
34
|
+
context_servers: {
|
|
35
|
+
riskmodels: {
|
|
36
|
+
command: {
|
|
37
|
+
path: "node",
|
|
38
|
+
args: [serverPath],
|
|
39
|
+
...(Object.keys(env).length ? { env } : {}),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function humanPath(client) {
|
|
46
|
+
if (client === "claude-desktop") {
|
|
47
|
+
return "~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows)";
|
|
48
|
+
}
|
|
49
|
+
if (client === "cursor") {
|
|
50
|
+
return ".cursor/mcp.json (workspace) or ~/.cursor/mcp.json (global)";
|
|
51
|
+
}
|
|
52
|
+
return "~/.config/zed/settings.json";
|
|
53
|
+
}
|
|
54
|
+
export function mcpConfigCommand() {
|
|
55
|
+
return new Command("mcp-config")
|
|
56
|
+
.description("Print a ready-to-paste MCP client config for Claude Desktop, Cursor, or Zed")
|
|
57
|
+
.option("-c, --client <name>", "claude-desktop | cursor | zed (default: claude-desktop)", "claude-desktop")
|
|
58
|
+
.option("-p, --mcp-server-path <path>", "Absolute path to RiskModels_API/mcp/dist/index.js", DEFAULT_MCP_SERVER_PLACEHOLDER)
|
|
59
|
+
.option("--embed-key", "Embed the API key from ~/.config/riskmodels/config.json in the env block. Default is to rely on the MCP server's fallback read of the CLI config file.")
|
|
60
|
+
.action(async (opts) => {
|
|
61
|
+
const client = (opts.client || "claude-desktop").toLowerCase();
|
|
62
|
+
if (!["claude-desktop", "cursor", "zed"].includes(client)) {
|
|
63
|
+
console.error(chalk.red(`Unknown client: ${client}. Use claude-desktop, cursor, or zed.`));
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let apiKey;
|
|
68
|
+
if (opts.embedKey) {
|
|
69
|
+
const cfg = await loadConfig();
|
|
70
|
+
apiKey = cfg?.apiKey?.trim();
|
|
71
|
+
if (!apiKey) {
|
|
72
|
+
console.error(chalk.yellow("warning: --embed-key requested but no apiKey in " +
|
|
73
|
+
configPath() +
|
|
74
|
+
". Run `riskmodels config init` first."));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const serverPath = opts.mcpServerPath || DEFAULT_MCP_SERVER_PLACEHOLDER;
|
|
78
|
+
let config;
|
|
79
|
+
if (client === "claude-desktop")
|
|
80
|
+
config = claudeDesktopConfig(serverPath, apiKey);
|
|
81
|
+
else if (client === "cursor")
|
|
82
|
+
config = cursorConfig(serverPath, apiKey);
|
|
83
|
+
else
|
|
84
|
+
config = zedConfig(serverPath, apiKey);
|
|
85
|
+
// Helpful human-readable guidance ONLY to stderr so stdout stays pipe-clean.
|
|
86
|
+
console.error(chalk.dim(`# Paste this into ${humanPath(client)}`));
|
|
87
|
+
if (!opts.embedKey) {
|
|
88
|
+
console.error(chalk.dim(`# The MCP server reads ~/.config/riskmodels/config.json automatically — no key needed in the env block.`));
|
|
89
|
+
}
|
|
90
|
+
if (serverPath === DEFAULT_MCP_SERVER_PLACEHOLDER) {
|
|
91
|
+
console.error(chalk.dim(`# Replace the placeholder path with the absolute path to your RiskModels_API/mcp/dist/index.js.`));
|
|
92
|
+
}
|
|
93
|
+
console.log(JSON.stringify(config, null, 2));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve path to the built MCP server entry (mcp/dist/index.js).
|
|
4
|
+
*
|
|
5
|
+
* Order:
|
|
6
|
+
* 1. RISKMODELS_MCP_SERVER_PATH
|
|
7
|
+
* 2. --mcp-server-path CLI option
|
|
8
|
+
* 3. ./mcp/dist/index.js relative to cwd (repo root when running from RiskModels_API)
|
|
9
|
+
* 4. ../../mcp/dist/index.js relative to cli/dist (monorepo dev: npm link / npm run from repo)
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveMcpServerPath(explicit?: string): string | null;
|
|
12
|
+
export declare function mcpServeCommand(): Command;
|