@riskmodels/mcp 1.0.1

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.
Files changed (38) hide show
  1. package/INSTALL.md +130 -0
  2. package/README.md +196 -0
  3. package/data/capabilities.json +924 -0
  4. package/data/openapi.json +6760 -0
  5. package/data/schema-paths.json +16 -0
  6. package/data/schemas/error-v1.json +49 -0
  7. package/data/schemas/estimate-v1.json +49 -0
  8. package/data/schemas/factor-correlation-request-v1.json +55 -0
  9. package/data/schemas/factor-correlation-v1.json +31 -0
  10. package/data/schemas/health-v1.json +85 -0
  11. package/data/schemas/l3-decomposition-v1.json +125 -0
  12. package/data/schemas/macro-factors-series-v1.json +45 -0
  13. package/data/schemas/portfolio-risk-index-v1.json +48 -0
  14. package/data/schemas/portfolio-risk-snapshot-v1.json +134 -0
  15. package/data/schemas/risk-metadata-v1.json +35 -0
  16. package/data/schemas/telemetry-v1.json +122 -0
  17. package/data/schemas/ticker-returns-v2.json +114 -0
  18. package/data/schemas/tickers-list-v1.json +64 -0
  19. package/data/whitepaper/01-core-claim.md +10 -0
  20. package/data/whitepaper/02-aapl-vs-nvda.md +9 -0
  21. package/data/whitepaper/03-hedging.md +7 -0
  22. package/data/whitepaper/examples-aapl-nvda-crwd.md +7 -0
  23. package/data/whitepaper/one-position-four-bets.md +14 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +2 -0
  26. package/dist/lib/mcp/tools/riskmodels-tools.d.ts +34 -0
  27. package/dist/lib/mcp/tools/riskmodels-tools.js +211 -0
  28. package/dist/mcp/src/index.d.ts +9 -0
  29. package/dist/mcp/src/index.js +19 -0
  30. package/dist/mcp/src/server.d.ts +25 -0
  31. package/dist/mcp/src/server.js +434 -0
  32. package/dist/server.d.ts +1 -0
  33. package/dist/server.js +1 -0
  34. package/package.json +39 -0
  35. package/scripts/write-dist-wrappers.mjs +29 -0
  36. package/src/index.ts +22 -0
  37. package/src/server.ts +555 -0
  38. package/tsconfig.json +15 -0
@@ -0,0 +1,25 @@
1
+ /**
2
+ * RiskModels API MCP server — transport-agnostic factory.
3
+ *
4
+ * `createMcpServer()` returns a fully-configured `McpServer` with all
5
+ * resources and tools registered. Callers attach a transport (stdio,
6
+ * Streamable HTTP, etc.) separately.
7
+ *
8
+ * Keep this file free of transport imports so both the stdio binary and
9
+ * a future hosted route (Next.js `/api/mcp/sse` or a Node sidecar) can
10
+ * import the same factory.
11
+ */
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ export type McpServerOptions = {
14
+ /** Override the API key resolution (env → CLI config → caller-supplied). */
15
+ apiKey?: string;
16
+ /** Override the API base URL (env → CLI config → default). */
17
+ apiBase?: string;
18
+ };
19
+ /**
20
+ * Build a fully-configured RiskModels MCP server.
21
+ *
22
+ * No transport is attached — caller is responsible for
23
+ * `server.connect(transport)` with whichever transport fits their runtime.
24
+ */
25
+ export declare function createMcpServer(opts?: McpServerOptions): McpServer;
@@ -0,0 +1,434 @@
1
+ /**
2
+ * RiskModels API MCP server — transport-agnostic factory.
3
+ *
4
+ * `createMcpServer()` returns a fully-configured `McpServer` with all
5
+ * resources and tools registered. Callers attach a transport (stdio,
6
+ * Streamable HTTP, etc.) separately.
7
+ *
8
+ * Keep this file free of transport imports so both the stdio binary and
9
+ * a future hosted route (Next.js `/api/mcp/sse` or a Node sidecar) can
10
+ * import the same factory.
11
+ */
12
+ import { readFileSync, existsSync } from "fs";
13
+ import { homedir } from "os";
14
+ import { join, dirname } from "path";
15
+ import { fileURLToPath } from "url";
16
+ import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
17
+ import { z } from "zod";
18
+ import { createRiskModelsSdk, registerRiskModelsPrompts, registerRiskModelsTools, registerRiskModelsWhitepaperResources, } from "../../lib/mcp/tools/riskmodels-tools.js";
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const DATA_DIR = join(__dirname, "..", "data");
21
+ const DEFAULT_API_BASE = "https://riskmodels.app";
22
+ const CLI_CONFIG_PATH = join(homedir(), ".config", "riskmodels", "config.json");
23
+ function resolveCredentials(opts = {}) {
24
+ if (opts.apiKey || opts.apiBase) {
25
+ const envBase = process.env.RISKMODELS_API_BASE ||
26
+ process.env.NEXT_PUBLIC_APP_URL ||
27
+ undefined;
28
+ return {
29
+ apiKey: opts.apiKey ?? process.env.RISKMODELS_API_KEY ?? null,
30
+ apiBase: (opts.apiBase || envBase || DEFAULT_API_BASE).replace(/\/$/, ""),
31
+ };
32
+ }
33
+ const envKey = process.env.RISKMODELS_API_KEY;
34
+ const envBase = process.env.RISKMODELS_API_BASE ||
35
+ process.env.NEXT_PUBLIC_APP_URL ||
36
+ undefined;
37
+ let cfg = {};
38
+ if (existsSync(CLI_CONFIG_PATH)) {
39
+ try {
40
+ cfg = JSON.parse(readFileSync(CLI_CONFIG_PATH, "utf-8"));
41
+ }
42
+ catch {
43
+ // ignore — corrupt config just falls through to env
44
+ }
45
+ }
46
+ const apiKey = envKey || cfg.apiKey || null;
47
+ const apiBase = (envBase || cfg.apiBaseUrl || DEFAULT_API_BASE).replace(/\/$/, "");
48
+ return { apiKey, apiBase };
49
+ }
50
+ const TOKEN_PRICE_USD = 0.00002;
51
+ function extractMeter(headers) {
52
+ const parseNum = (h) => (h == null ? undefined : Number(h));
53
+ const balanceTokens = parseNum(headers.get("X-Balance-Remaining"));
54
+ const balanceUsd = balanceTokens !== undefined && !Number.isNaN(balanceTokens)
55
+ ? +(balanceTokens * TOKEN_PRICE_USD).toFixed(4)
56
+ : undefined;
57
+ return {
58
+ _cost_usd: parseNum(headers.get("X-API-Cost-USD")),
59
+ _balance_remaining_usd: balanceUsd,
60
+ _monthly_spend_cap_usd: parseNum(headers.get("X-Monthly-Spend-Cap-USD")),
61
+ _current_monthly_spend_usd: parseNum(headers.get("X-Current-Monthly-Spend-USD")),
62
+ _rate_limit_remaining: parseNum(headers.get("X-RateLimit-Remaining")),
63
+ _data_as_of: headers.get("X-Data-As-Of") || undefined,
64
+ _latency_ms: parseNum(headers.get("X-Data-Fetch-Latency-Ms")),
65
+ _request_id: headers.get("X-Request-ID") || undefined,
66
+ _pricing_tier: headers.get("X-Pricing-Tier") || undefined,
67
+ };
68
+ }
69
+ async function apiCall(opts, method, path, init = {}) {
70
+ const { apiKey, apiBase } = resolveCredentials(opts);
71
+ if (!apiKey) {
72
+ return {
73
+ status: 0,
74
+ data: null,
75
+ meter: {},
76
+ error: "RiskModels API key not found. Set RISKMODELS_API_KEY in the MCP client env, or run `riskmodels config init` to store it at ~/.config/riskmodels/config.json.",
77
+ };
78
+ }
79
+ const url = new URL(`${apiBase}/api${path}`);
80
+ if (init.query) {
81
+ for (const [k, v] of Object.entries(init.query)) {
82
+ if (v != null)
83
+ url.searchParams.set(k, v);
84
+ }
85
+ }
86
+ try {
87
+ const res = await fetch(url.toString(), {
88
+ method,
89
+ headers: {
90
+ Authorization: `Bearer ${apiKey}`,
91
+ "Content-Type": "application/json",
92
+ Accept: "application/json",
93
+ },
94
+ body: init.body ? JSON.stringify(init.body) : undefined,
95
+ });
96
+ const meter = extractMeter(res.headers);
97
+ let data = null;
98
+ try {
99
+ data = await res.json();
100
+ }
101
+ catch {
102
+ data = { error: "Non-JSON response", status: res.status };
103
+ }
104
+ return { status: res.status, data, meter };
105
+ }
106
+ catch (e) {
107
+ return {
108
+ status: 0,
109
+ data: null,
110
+ meter: {},
111
+ error: `Network error calling RiskModels API: ${e.message}`,
112
+ };
113
+ }
114
+ }
115
+ function wrapWithMeter(data, meter) {
116
+ if (data && typeof data === "object" && !Array.isArray(data)) {
117
+ const record = data;
118
+ const metaFromBody = record._metadata;
119
+ const envelope = {
120
+ ...meter,
121
+ _data_as_of: meter._data_as_of || metaFromBody?.data_as_of,
122
+ _data_source: metaFromBody?.data_source,
123
+ };
124
+ const clean = {};
125
+ for (const [k, v] of Object.entries(envelope)) {
126
+ if (v !== undefined && v !== null && !Number.isNaN(v)) {
127
+ clean[k] = v;
128
+ }
129
+ }
130
+ return JSON.stringify({ ...record, ...clean }, null, 2);
131
+ }
132
+ return JSON.stringify({ data, ...meter }, null, 2);
133
+ }
134
+ function loadJson(relativePath) {
135
+ const p = join(DATA_DIR, relativePath);
136
+ if (!existsSync(p))
137
+ return null;
138
+ try {
139
+ return JSON.parse(readFileSync(p, "utf-8"));
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ function loadText(relativePath) {
146
+ const p = join(DATA_DIR, relativePath);
147
+ if (!existsSync(p))
148
+ return null;
149
+ try {
150
+ return readFileSync(p, "utf-8");
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ }
156
+ /**
157
+ * Build a fully-configured RiskModels MCP server.
158
+ *
159
+ * No transport is attached — caller is responsible for
160
+ * `server.connect(transport)` with whichever transport fits their runtime.
161
+ */
162
+ export function createMcpServer(opts = {}) {
163
+ const server = new McpServer({
164
+ name: "riskmodels-api",
165
+ version: "1.0.0",
166
+ });
167
+ const credentials = resolveCredentials(opts);
168
+ const sdk = createRiskModelsSdk({
169
+ apiKey: credentials.apiKey,
170
+ apiBase: credentials.apiBase,
171
+ });
172
+ // --- Resources ---
173
+ // riskmodels:///manifest — agent manifest (fetch from API if base URL set, else static)
174
+ server.registerResource("manifest", "riskmodels:///manifest", {
175
+ title: "RiskModels Agent Manifest",
176
+ description: "Agent Protocol service discovery manifest (from API when base URL set)",
177
+ mimeType: "application/json",
178
+ }, async (uri) => {
179
+ const { apiBase } = resolveCredentials(opts);
180
+ try {
181
+ const res = await fetch(`${apiBase}/.well-known/agent-manifest.json`);
182
+ if (res.ok) {
183
+ const json = await res.json();
184
+ return {
185
+ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(json, null, 2) }],
186
+ };
187
+ }
188
+ }
189
+ catch {
190
+ // fall through to static
191
+ }
192
+ const capabilities = loadJson("capabilities.json");
193
+ const payload = {
194
+ service: { name: "RiskModels", version: "2.0.0-agent" },
195
+ capabilities: capabilities || [],
196
+ _note: "Set RISKMODELS_API_BASE or NEXT_PUBLIC_APP_URL to fetch live manifest.",
197
+ };
198
+ return {
199
+ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(payload, null, 2) }],
200
+ };
201
+ });
202
+ // riskmodels:///capabilities — list of API capabilities
203
+ server.registerResource("capabilities", "riskmodels:///capabilities", {
204
+ title: "RiskModels API Capabilities",
205
+ description: "List of API capabilities with endpoints, parameters, pricing",
206
+ mimeType: "application/json",
207
+ }, async (uri) => {
208
+ const data = loadJson("capabilities.json");
209
+ const text = data ? JSON.stringify(data, null, 2) : JSON.stringify({ error: "capabilities.json not found" });
210
+ return {
211
+ contents: [{ uri: uri.href, mimeType: "application/json", text }],
212
+ };
213
+ });
214
+ // riskmodels:///schemas/list — list of schema paths
215
+ server.registerResource("schemas-list", "riskmodels:///schemas/list", {
216
+ title: "RiskModels Schema Paths",
217
+ description: "List of available response schema paths",
218
+ mimeType: "application/json",
219
+ }, async (uri) => {
220
+ const data = loadJson("schema-paths.json");
221
+ const text = data ? JSON.stringify(data, null, 2) : JSON.stringify([]);
222
+ return {
223
+ contents: [{ uri: uri.href, mimeType: "application/json", text }],
224
+ };
225
+ });
226
+ // riskmodels:///schemas/{path} — individual schema
227
+ const schemaPaths = () => {
228
+ const list = loadJson("schema-paths.json");
229
+ return list || [];
230
+ };
231
+ server.registerResource("schema-by-path", new ResourceTemplate("riskmodels:///schemas/{path}", {
232
+ list: async () => ({
233
+ resources: schemaPaths().map((p) => ({
234
+ uri: `riskmodels:///schemas/${encodeURIComponent(p.replace(/^\/schemas\//, ""))}`,
235
+ name: p.split("/").pop() || p,
236
+ })),
237
+ }),
238
+ }), {
239
+ title: "RiskModels Response Schema",
240
+ description: "JSON schema for an API response by path (e.g. ticker-returns-v2.json)",
241
+ mimeType: "application/json",
242
+ }, async (uri, variables) => {
243
+ const pathVar = variables.path;
244
+ const pathSeg = typeof pathVar === "string" ? pathVar : Array.isArray(pathVar) ? pathVar[0] : "";
245
+ const normalized = pathSeg.startsWith("/schemas/") ? pathSeg : `/schemas/${pathSeg}`;
246
+ const filename = normalized.split("/").pop() || pathSeg;
247
+ const data = loadJson(join("schemas", filename));
248
+ const text = data ? JSON.stringify(data, null, 2) : JSON.stringify({ error: `Schema not found: ${pathSeg}` });
249
+ return {
250
+ contents: [{ uri: uri.href, mimeType: "application/json", text }],
251
+ };
252
+ });
253
+ // riskmodels:///openapi — OpenAPI spec if present
254
+ server.registerResource("openapi", "riskmodels:///openapi", {
255
+ title: "RiskModels OpenAPI Spec",
256
+ description: "OpenAPI 3.x specification for the API",
257
+ mimeType: "application/json",
258
+ }, async (uri) => {
259
+ const yaml = loadText("openapi.yaml");
260
+ const json = loadJson("openapi.json");
261
+ if (json) {
262
+ return {
263
+ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(json, null, 2) }],
264
+ };
265
+ }
266
+ if (yaml) {
267
+ return {
268
+ contents: [{ uri: uri.href, mimeType: "application/yaml", text: yaml }],
269
+ };
270
+ }
271
+ return {
272
+ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ info: { title: "RiskModels API" }, _note: "Add openapi.json or openapi.yaml to mcp/data/" }) }],
273
+ };
274
+ });
275
+ registerRiskModelsWhitepaperResources(server, DATA_DIR);
276
+ registerRiskModelsPrompts(server);
277
+ // --- Tools ---
278
+ registerRiskModelsTools(sdk, server);
279
+ server.registerTool("riskmodels_list_endpoints", {
280
+ title: "List RiskModels API Endpoints",
281
+ description: "List all public API capabilities (id, name, method, endpoint, short description)",
282
+ inputSchema: z.object({}).optional(),
283
+ }, async () => {
284
+ const capabilities = loadJson("capabilities.json");
285
+ if (!capabilities) {
286
+ return { content: [{ type: "text", text: JSON.stringify({ error: "capabilities.json not found" }) }] };
287
+ }
288
+ const list = capabilities.map((c) => ({
289
+ id: c.id,
290
+ name: c.name,
291
+ method: c.method,
292
+ endpoint: c.endpoint,
293
+ description: (c.description || "").slice(0, 80),
294
+ }));
295
+ return { content: [{ type: "text", text: JSON.stringify(list, null, 2) }] };
296
+ });
297
+ server.registerTool("riskmodels_get_capability", {
298
+ title: "Get RiskModels Capability Details",
299
+ description: "Get full capability details (parameters, pricing, examples) by id",
300
+ inputSchema: z.object({
301
+ id: z.string().describe("Capability id (e.g. ticker-returns, risk-decomposition)"),
302
+ }),
303
+ }, async ({ id }) => {
304
+ const capabilities = loadJson("capabilities.json");
305
+ if (!capabilities) {
306
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Capabilities not loaded" }) }] };
307
+ }
308
+ const cap = capabilities.find((c) => c.id === id);
309
+ if (!cap) {
310
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown capability: ${id}`, available: capabilities.map((c) => c.id) }) }] };
311
+ }
312
+ return { content: [{ type: "text", text: JSON.stringify(cap, null, 2) }] };
313
+ });
314
+ server.registerTool("riskmodels_get_schema", {
315
+ title: "Get RiskModels Response Schema",
316
+ description: "Get JSON schema for an API response by path (e.g. ticker-returns-v2.json)",
317
+ inputSchema: z.object({
318
+ path: z.string().describe("Schema path or filename (e.g. ticker-returns-v2.json or /schemas/ticker-returns-v2.json)"),
319
+ }),
320
+ }, async ({ path: pathArg }) => {
321
+ const pathSeg = pathArg.replace(/^\/schemas\//, "");
322
+ const filename = pathSeg.endsWith(".json") ? pathSeg : `${pathSeg}.json`;
323
+ const data = loadJson(join("schemas", filename));
324
+ if (!data) {
325
+ const paths = loadJson("schema-paths.json") || [];
326
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Schema not found: ${filename}`, available: paths }) }] };
327
+ }
328
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
329
+ });
330
+ // --- Live data tools (require API key via env or ~/.config/riskmodels/config.json) ---
331
+ server.registerTool("get_l3_decomposition", {
332
+ title: "L3 Hierarchical Risk Decomposition",
333
+ description: "Daily EOD hierarchical orthogonal decomposition for a single ticker: market → sector → subsector → residual. Returns parallel time-series arrays plus hedge ratios. Historical data from GCP zarr; latest snapshot from Supabase. Data freshness: daily after US market close (see _data_as_of).",
334
+ inputSchema: z.object({
335
+ ticker: z.string().describe("Stock ticker symbol, e.g. NVDA, AAPL"),
336
+ market_factor_etf: z
337
+ .string()
338
+ .optional()
339
+ .default("SPY")
340
+ .describe("Market factor ETF for L1 (default SPY)"),
341
+ years: z
342
+ .number()
343
+ .int()
344
+ .min(1)
345
+ .max(15)
346
+ .optional()
347
+ .default(1)
348
+ .describe("Years of daily history to return (1–15)"),
349
+ }),
350
+ }, async ({ ticker, market_factor_etf, years }) => {
351
+ const { status, data, meter, error } = await apiCall(opts, "GET", "/l3-decomposition", {
352
+ query: {
353
+ ticker,
354
+ market_factor_etf: market_factor_etf || "SPY",
355
+ years: String(years ?? 1),
356
+ },
357
+ });
358
+ if (error) {
359
+ return { content: [{ type: "text", text: JSON.stringify({ error }) }] };
360
+ }
361
+ if (status >= 400) {
362
+ return { content: [{ type: "text", text: JSON.stringify({ error: `API ${status}`, detail: data }) }] };
363
+ }
364
+ return { content: [{ type: "text", text: wrapWithMeter(data, meter) }] };
365
+ });
366
+ server.registerTool("get_metrics", {
367
+ title: "Latest Risk Metrics Snapshot",
368
+ description: "Latest daily EOD risk metrics for a ticker from the Supabase _latest table: L1/L2/L3 hedge ratios (SPY, sector ETF, subsector ETF), explained-risk fractions, daily volatility, price close, market cap. Single-row snapshot. Data freshness: daily after US market close.",
369
+ inputSchema: z.object({
370
+ ticker: z.string().describe("Stock ticker symbol, e.g. NVDA, AAPL"),
371
+ }),
372
+ }, async ({ ticker }) => {
373
+ const { status, data, meter, error } = await apiCall(opts, "GET", `/metrics/${encodeURIComponent(ticker.toUpperCase())}`);
374
+ if (error) {
375
+ return { content: [{ type: "text", text: JSON.stringify({ error }) }] };
376
+ }
377
+ if (status >= 400) {
378
+ return { content: [{ type: "text", text: JSON.stringify({ error: `API ${status}`, detail: data }) }] };
379
+ }
380
+ return { content: [{ type: "text", text: wrapWithMeter(data, meter) }] };
381
+ });
382
+ server.registerTool("get_portfolio_risk_snapshot", {
383
+ title: "Portfolio Risk Snapshot",
384
+ description: "Bundled portfolio risk report for up to 100 positions: variance decomposition (market / sector / subsector / residual / systematic), portfolio 23-day volatility, and optional diversification analytics with sector/subsector ETF correlation matrices. Returns JSON by default. Response is cached per-user per-portfolio for 1 hour.",
385
+ inputSchema: z.object({
386
+ positions: z
387
+ .array(z.object({
388
+ ticker: z.string(),
389
+ weight: z.number().positive(),
390
+ }))
391
+ .min(1)
392
+ .max(100)
393
+ .describe("Positions as { ticker, weight } pairs. Weights need not sum to 1."),
394
+ title: z.string().max(200).optional(),
395
+ as_of_date: z
396
+ .string()
397
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
398
+ .optional()
399
+ .describe("YYYY-MM-DD override for the snapshot date"),
400
+ include_diversification: z
401
+ .boolean()
402
+ .optional()
403
+ .default(false)
404
+ .describe("Include diversification metrics (adds latency)"),
405
+ window_days: z
406
+ .number()
407
+ .int()
408
+ .min(20)
409
+ .max(2000)
410
+ .optional()
411
+ .default(252)
412
+ .describe("Rolling window in trading days for diversification"),
413
+ }),
414
+ }, async ({ positions, title, as_of_date, include_diversification, window_days }) => {
415
+ const body = { positions, format: "json" };
416
+ if (title)
417
+ body.title = title;
418
+ if (as_of_date)
419
+ body.as_of_date = as_of_date;
420
+ if (include_diversification)
421
+ body.include_diversification = include_diversification;
422
+ if (window_days)
423
+ body.window_days = window_days;
424
+ const { status, data, meter, error } = await apiCall(opts, "POST", "/portfolio/risk-snapshot", { body });
425
+ if (error) {
426
+ return { content: [{ type: "text", text: JSON.stringify({ error }) }] };
427
+ }
428
+ if (status >= 400) {
429
+ return { content: [{ type: "text", text: JSON.stringify({ error: `API ${status}`, detail: data }) }] };
430
+ }
431
+ return { content: [{ type: "text", text: wrapWithMeter(data, meter) }] };
432
+ });
433
+ return server;
434
+ }
@@ -0,0 +1 @@
1
+ export * from './mcp/src/server.js';
package/dist/server.js ADDED
@@ -0,0 +1 @@
1
+ export * from './mcp/src/server.js';
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@riskmodels/mcp",
3
+ "version": "1.0.1",
4
+ "description": "MCP server for RiskModels: decompose US equities into four-bet variance shares with ETF hedge ratios",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "riskmodels-mcp": "./dist/index.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ },
15
+ "./server": {
16
+ "import": "./dist/server.js",
17
+ "types": "./dist/server.d.ts"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "tsc && node scripts/write-dist-wrappers.mjs",
22
+ "start": "node dist/index.js"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.0.0",
32
+ "@riskmodels/sdk": "^0.1.0",
33
+ "zod": "^3.23.8"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^20.6.2",
37
+ "typescript": "^5.2.2"
38
+ }
39
+ }
@@ -0,0 +1,29 @@
1
+ import { mkdirSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ const distDir = join(process.cwd(), "dist");
5
+ mkdirSync(distDir, { recursive: true });
6
+
7
+ writeFileSync(
8
+ join(distDir, "index.js"),
9
+ "#!/usr/bin/env node\nimport './mcp/src/index.js';\n",
10
+ "utf-8",
11
+ );
12
+
13
+ writeFileSync(
14
+ join(distDir, "server.js"),
15
+ "export * from './mcp/src/server.js';\n",
16
+ "utf-8",
17
+ );
18
+
19
+ writeFileSync(
20
+ join(distDir, "index.d.ts"),
21
+ "export {};\n",
22
+ "utf-8",
23
+ );
24
+
25
+ writeFileSync(
26
+ join(distDir, "server.d.ts"),
27
+ "export * from './mcp/src/server.js';\n",
28
+ "utf-8",
29
+ );
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * RiskModels API MCP server — stdio entry point.
4
+ *
5
+ * Server construction and tool registrations live in `./server.ts` so the
6
+ * hosted Next.js route (`/api/mcp/sse`) can reuse them. This file wires only
7
+ * the stdio transport.
8
+ */
9
+
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import { createMcpServer } from "./server.js";
12
+
13
+ async function main() {
14
+ const server = createMcpServer();
15
+ const transport = new StdioServerTransport();
16
+ await server.connect(transport);
17
+ }
18
+
19
+ main().catch((err) => {
20
+ console.error(err);
21
+ process.exit(1);
22
+ });